同じ部品へ複数のインスタンスを差し替えるときは、置換先と置換対象を分けて登録します。このプラグインは登録したインスタンスにswapComponentを実行し、成功・失敗の件数を通知します。選択するノードが部品内部のレイヤーでも、最も近い親インスタンスを探します。
通常の入れ替えと一括処理の使い分け
少数の部品ならFigmaの標準のインスタンス入れ替えでも対応できます。繰り返し同じ差し替えを行う場面で、自作の登録UIを使います。swapComponentは標準UIと同様の規則でオーバーライドを引き継ぎますが、すべての内容が無条件に保持される保証ではありません。
操作の順序
- 複製ファイルで置換先のメインコンポーネントを選び、sourceの登録を押します。
- 入れ替えるインスタンスを複数選び、targetの追加を押します。
- 一覧を確認します。同じIDを再登録しても1件にまとめます。
- 差し替え実行を押して確認し、結果の件数を見ます。
- 文字、画像、サイズ、ネストしたインスタンスなどを確認します。
置換先にはコンポーネント、コンポーネントセットの既定バリアント、インスタンスの参照元を利用できます。元の例の「複数選択可能」という説明に対して先頭1件しか登録しない処理を、選択全件の登録へ修正しています。
ファイルを作成する
FigmaのNew pluginで生成したmanifestを基に、次の設定を使います。掲載例ではidを省略しています。自分のmanifestに割り当てられたidがある場合はその値を保ち、サンプルの任意文字列に置き換えません。3ファイルを同じフォルダへ保存してください。
manifest.json
{
"name": "インスタンスの入れ替え",
"api": "1.0.0",
"main": "code.js",
"ui": "ui.html",
"editorType": [
"figma"
],
"documentAccess": "dynamic-page",
"networkAccess": {
"allowedDomains": [
"none"
]
}
}code.js
function isInstance(n){ return n && n.type === "INSTANCE"; }
function isComponent(n){ return n && n.type === "COMPONENT"; }
function isComponentSet(n){ return n && n.type === "COMPONENT_SET"; }
function nearestInstance(node){
var cur = node;
while (cur) {
if (isInstance(cur)) return cur;
cur = cur.parent;
}
return null;
}
async function getComponentFromNode(node){
if (!node) return null;
if (isInstance(node)) return await node.getMainComponentAsync();
if (isComponent(node)) return node;
if (isComponentSet(node)) {
if (node.defaultVariant) return node.defaultVariant;
var i; for (i=0; i<node.children.length; i++) {
var c = node.children[i];
if (c && c.type === "COMPONENT") return c;
}
}
return null;
}
async function resolveComponent(componentNodeId, componentKey){
if (componentNodeId && componentNodeId !== "") {
var local = await figma.getNodeByIdAsync(componentNodeId);
if (local && local.type === "COMPONENT") return local;
}
if (componentKey && componentKey !== "") {
try {
var imported = await figma.importComponentByKeyAsync(componentKey);
if (imported) return imported;
} catch (e) {}
}
return null;
}
figma.showUI(__html__, { width: 280, height: 220 });
var stored = {
targets: [], // {id, name}
sourceId: "",
sourceKey: "",
sourceName: ""
};
function postState(){
figma.ui.postMessage({
type: "state",
payload: {
targets: stored.targets,
sourceName: stored.sourceName,
hasTarget: stored.targets.length > 0,
hasSource: (stored.sourceId !== "" || stored.sourceKey !== "")
}
});
}
let busy = false;
figma.ui.onmessage = async function(msg){
if (!msg || busy) return;
busy = true;
try {
if (msg.type === "capture-source") {
var sel = figma.currentPage.selection;
if (!sel || sel.length === 0) { figma.notify("source を選択してください"); return; }
var comp = await getComponentFromNode(sel[0]);
if (!comp) { figma.notify("メインコンポーネントを取得できません"); return; }
stored.sourceId = comp.id || "";
stored.sourceKey = comp.key || "";
stored.sourceName = comp.name || "(no name)";
postState();
return;
}
if (msg.type === "capture-target") {
var sel = figma.currentPage.selection;
if (!sel.length) { figma.notify("target を選択してください"); return; }
for (const node of sel) {
const inst = nearestInstance(node);
if (inst && !stored.targets.some(t => t.id === inst.id))
stored.targets.push({ id: inst.id, name: inst.name });
}
postState();
return;
}
if (msg.type === "clear-targets") {
stored.targets = [];
postState();
return;
}
if (msg.type === "swap-now") {
if (stored.targets.length === 0) { figma.notify("target 未登録"); return; }
if (!stored.sourceId && !stored.sourceKey) { figma.notify("source 未登録"); return; }
var dest = await resolveComponent(stored.sourceId, stored.sourceKey);
if (!dest) { figma.notify("source を解決できません"); return; }
var ok = 0, fail = 0;
for (var i=0; i<stored.targets.length; i++){
var t = await figma.getNodeByIdAsync(stored.targets[i].id);
if (!t || t.type !== "INSTANCE") { fail++; continue; }
try {
t.swapComponent(dest);
ok++;
} catch(e){ fail++; }
}
figma.notify("差し替え完了: 成功 " + ok + " 件 / 失敗 " + fail + " 件");
return;
}
if (msg.type === "close") figma.closePlugin();
} catch (_) { figma.notify("処理に失敗しました。対象を登録し直してください", {error:true}); }
finally { busy = false; }
};
postState();ui.html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Swap Multi Target</title>
<style>
body { font-family: Inter, sans-serif; margin: 12px; font-size: 12px; }
.row { display: flex; align-items: center; margin-bottom: 8px; }
.val { flex: 1; padding: 6px 8px; background: #f5f5f5; border-radius: 6px; }
button { margin-left: 6px; font-size: 12px; padding: 6px 10px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer; }
button.primary { background: #18a0fb; color: #fff; border-color: #18a0fb; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
ul { margin: 4px 0; padding-left: 16px; }
</style>
</head>
<body>
<div class="row">
<div class="val" id="sourceVal">source 未登録</div>
<button id="btnCapSource">登録</button>
</div>
<div class="row">
<div class="val">target 複数可</div>
<button id="btnCapTarget">追加</button>
<button id="btnClearTargets">クリア</button>
</div>
<ul id="targetList"></ul>
<div class="row">
<button id="btnSwap" class="primary" disabled style="flex:1;">差し替え実行</button>
</div>
<script>
var $sourceVal = document.getElementById('sourceVal');
var $targetList = document.getElementById('targetList');
var $btnSwap = document.getElementById('btnSwap');
function post(type, payload){
parent.postMessage({ pluginMessage: { type: type, payload: payload||{} } }, '*');
}
document.getElementById('btnCapSource').onclick = function(){ post('capture-source'); };
document.getElementById('btnCapTarget').onclick = function(){ post('capture-target'); };
document.getElementById('btnClearTargets').onclick = function(){ post('clear-targets'); };
$btnSwap.onclick = function(){ if (confirm('登録したインスタンスを差し替えますか?')) post('swap-now'); };
onmessage = function(e){
var m = e.data.pluginMessage;
if (!m || m.type !== 'state') return;
$sourceVal.textContent = m.payload.sourceName || "source 未登録";
$btnSwap.disabled = !(m.payload.hasSource && m.payload.hasTarget);
$targetList.innerHTML = "";
for (var i=0; i<m.payload.targets.length; i++){
var li = document.createElement("li");
li.textContent = m.payload.targets[i].name;
$targetList.appendChild(li);
}
};
</script>
</body>
</html>失敗する場合と注意点
保存したIDからノードを取得し直すため、登録後に削除されたものは失敗として数えます。ローカルの置換先が解決できない場合は、キーから公開コンポーネントの読み込みを試みます。公開状態と権限によって失敗する場合があります。
親インスタンスとその内部のインスタンスを両方登録すると、親の差し替えで子の構造が変わる場合があります。最初は同じ階層の対象に絞り、差し替え後に残りを登録し直してください。実行中の別操作は処理しないガードを入れていますが、共同編集者の変更まで停止するものではありません。
名前だけをそろえる場合は参照元との名前比較を使い分けます。
元記事の操作例
以下は元記事で掲載していた開発時の画面です。今回のコード改稿後に撮影した検証画像ではありません。現行の表示や変更結果は、複製したファイルで確認してください。


検証範囲と関連ガイド
今回確認したのは掲載コードの構文と、該当する入力・例外処理のローカル模擬検証です。Figmaデスクトップアプリ上での動作確認とは区別しています。改稿後のコードと、元記事が参照していたGitHub上の版が一致するとは限りません。
開発環境と登録手順はFigmaプラグイン開発入門、公開はCommunityへの申請を参照してください。
