Figmaの外部スタイル参照を調べるには、適用されたstyle IDからスタイルを取得し、remoteがtrueか確認します。参照が見つかるレイヤーをCSVに並べれば、ライブラリの整理前に利用箇所を確認できます。
以下は読み取り用のプラグイン例です。現在のページと全ページを選べるようにし、非同期のスタイル取得、文字範囲ごとのスタイル、取得できない参照を扱います。スタイルの解除やレイヤーの変更は行いません。
検出できる対象と結果の読み方
| 対象 | 確認する参照 |
|---|---|
| 塗り | fillStyleId、テキストの文字範囲のfill style |
| 線 | strokeStyleId |
| エフェクト | effectStyleId |
| レイアウトグリッド | gridStyleId |
| 文字 | textStyleId、文字範囲ごとのtext style |
ローカルのスタイルはCSVから除き、外部参照をexternal、取得失敗や取得結果がnullの参照をunresolvedとして出力します。取得不能を「外部参照なし」と扱わないための区別です。unresolvedだけではリンク切れや削除を断定できないので、該当レイヤーとアクセス条件を確認してください。
Variablesへの直接のバインド、スタイルを解除した値、外部コンポーネントの利用全体は、この監査の対象に含みません。ライブラリ名を推測することも避け、取得できるstyle ID・名前・キーで照合します。BaseStyleのremoteとキー
次は2025年8月の旧版の出力例です。改稿版の列や動作を新たにFigma上で検証した画面ではありません。


manifest.jsonを設定する
プラグイン開発の始め方に沿ってDesign用プラグインを作成します。生成されたidを保ち、以下の項目を設定してください。このJSONは変更する項目の例です。
{
"name": "External Style Audit",
"api": "1.0.0",
"main": "code.js",
"ui": "ui.html",
"editorType": ["figma"],
"documentAccess": "dynamic-page",
"networkAccess": { "allowedDomains": ["none"] }
}ユーザー情報は使わないため、currentuser権限は追加していません。全ページを調べる場合だけloadAllPagesAsync()を呼びます。大きなファイルでは読み込みと走査に時間がかかるため、最初は現在のページを選びます。Manifest、動的ページ読み込みへの移行
code.js:外部・取得不能な参照の抽出
以下をcode.jsへ保存します。旧版の同期getStyleById()はdynamic-pageで例外になるため、getStyleByIdAsync()を使っています。
figma.showUI(__html__, { width: 360, height: 210 });
let busy = false;
function csvCell(value) {
let text = String(value == null ? '' : value);
if (/^[=+@\-\t\r\n]/.test(text)) text = "'" + text;
return '"' + text.replace(/"/g, '""') + '"';
}
async function audit(scope) {
if (scope === 'all') await figma.loadAllPagesAsync();
const cache = new Map();
const rows = [];
async function resolve(id) {
if (!cache.has(id)) {
cache.set(id, figma.getStyleByIdAsync(id).then(style => {
if (!style) return { status: 'unresolved', name: '', key: '' };
return { status: style.remote ? 'external' : 'local',
name: style.name, key: style.key };
}).catch(() => ({ status: 'unresolved', name: '', key: '' })));
}
return cache.get(id);
}
async function inspect(node, pageName) {
const refs = new Map();
function add(id, type) {
if (typeof id === 'string' && id) refs.set(type + ':' + id, { id, type });
}
for (const [property, type] of [
['fillStyleId', 'Fill'], ['strokeStyleId', 'Stroke'],
['effectStyleId', 'Effect'], ['gridStyleId', 'Grid'],
['textStyleId', 'Text']
]) {
if (property in node) add(node[property], type);
}
if (node.type === 'TEXT' && node.characters.length > 0) {
for (const segment of node.getStyledTextSegments(['textStyleId', 'fillStyleId'])) {
add(segment.textStyleId, 'Text');
add(segment.fillStyleId, 'Fill');
}
}
for (const ref of refs.values()) {
const style = await resolve(ref.id);
if (style.status !== 'local') {
rows.push([pageName, node.name, node.id, ref.type,
ref.id, style.name, style.key, style.status]);
}
}
}
async function walk(node, pageName) {
await inspect(node, pageName);
if ('children' in node) {
for (const child of node.children) await walk(child, pageName);
}
}
const pages = scope === 'all' ? figma.root.children : [figma.currentPage];
for (const page of pages) await walk(page, page.name);
const headers = ['Page', 'Layer', 'Node ID', 'Type', 'Style ID',
'Style Name', 'Style Key', 'Status'];
const csv = [headers, ...rows].map(row => row.map(csvCell).join(',')).join('\r\n');
return { csv, count: rows.length,
unresolved: rows.filter(row => row[7] === 'unresolved').length };
}
figma.ui.onmessage = async msg => {
if (!msg || msg.type !== 'export' || busy) return;
if (msg.scope !== 'current' && msg.scope !== 'all') return;
busy = true;
try {
const result = await audit(msg.scope);
figma.ui.postMessage({ type: 'result', ...result });
} catch (error) {
figma.ui.postMessage({ type: 'error',
message: '走査に失敗しました。完了した監査として扱わず、コンソールを確認してください。' });
console.error(error);
} finally {
busy = false;
}
};テキスト全体のstyle IDがfigma.mixedでも、getStyledTextSegments()から範囲ごとの参照を取得します。同じノード・同じ種類・同じstyle IDは1行にまとめるため、文字範囲ごとの適用位置まではCSVへ出しません。文字範囲のAPI、非同期のスタイル取得
ui.html:走査範囲の選択とCSV保存
<!DOCTYPE html>
<html lang="ja">
<meta charset="utf-8">
<label>範囲 <select id="scope">
<option value="current">現在のページ</option>
<option value="all">全ページ</option>
</select></label>
<button id="run" type="button">CSVを作る</button>
<p id="status" role="status"></p>
<script>
const run = document.getElementById('run');
const statusEl = document.getElementById('status');
run.onclick = () => {
run.disabled = true;
statusEl.textContent = '走査中';
parent.postMessage({ pluginMessage: {
type: 'export', scope: document.getElementById('scope').value
} }, '*');
};
window.onmessage = event => {
const msg = event.data && event.data.pluginMessage;
if (!msg || typeof msg !== 'object') return;
if (msg.type === 'error') {
run.disabled = false;
statusEl.textContent = msg.message;
}
if (msg.type !== 'result') return;
try {
const url = URL.createObjectURL(new Blob(['\uFEFF', msg.csv], {
type: 'text/csv;charset=utf-8'
}));
const link = document.createElement('a');
link.href = url;
link.download = 'external-styles.csv';
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
statusEl.textContent = msg.count + '行(取得不能' + msg.unresolved +
'行)。保存を要求しました。保存先を確認してください。';
} catch (error) {
statusEl.textContent = '保存を開始できませんでした';
} finally {
run.disabled = false;
}
};
</script>
</html>CSVにはUTF-8のBOMを付け、カンマ・改行・引用符を含む名前を引用符で囲みます。表計算で数式と誤解される先頭文字にはアポストロフィを付けます。ダウンロード要求だけで保存成功を断定せず、保存したファイルと件数を確認してください。
実行後に確認すること
- 検証用ファイルで現在のページを走査します。
- 既知の外部スタイルが
externalに含まれ、ローカルのスタイルが除外されるか確認します。 - 混在テキストの参照と、
unresolvedの行を確認します。 - 全ページへ広げ、各ページの既知の対象と照合します。
- CSVを開き、日本語・引用符・改行・node ID・行数を確認します。
0行は、この走査範囲で対象のstyle IDが見つからなかったことを意味します。Variablesや切り離した値まで含めた「外部依存がない」という保証にはなりません。2026年10月3日のNode.js上の模擬APIで、混在テキスト、ローカルの除外、外部・取得不能の区別、取得のキャッシュ、全ページの読み込み呼び出し、CSVの引用符と数式対策を確認しました。改稿版のFigma上の実行とブラウザーでのCSV保存は未検証です。
旧版のコード資料は履歴として参照できます。今回の非同期API・文字範囲対応はこの記事のコードを基準にしてください。スタイルの整理前にはスタイルの作成・解除も確認します。
