Amazonで商品を見るセール会場へ

Figmaインスタンスを一括で入れ替えるプラグイン|対象登録と差し替え

同じ部品へ複数のインスタンスを差し替えるときは、置換先と置換対象を分けて登録します。このプラグインは登録したインスタンスにswapComponentを実行し、成功・失敗の件数を通知します。選択するノードが部品内部のレイヤーでも、最も近い親インスタンスを探します。

目次

通常の入れ替えと一括処理の使い分け

少数の部品ならFigmaの標準のインスタンス入れ替えでも対応できます。繰り返し同じ差し替えを行う場面で、自作の登録UIを使います。swapComponentは標準UIと同様の規則でオーバーライドを引き継ぎますが、すべての内容が無条件に保持される保証ではありません。

出典:標準の入れ替え、swapComponent API。

操作の順序

  1. 複製ファイルで置換先のメインコンポーネントを選び、sourceの登録を押します。
  2. 入れ替えるインスタンスを複数選び、targetの追加を押します。
  3. 一覧を確認します。同じIDを再登録しても1件にまとめます。
  4. 差し替え実行を押して確認し、結果の件数を見ます。
  5. 文字、画像、サイズ、ネストしたインスタンスなどを確認します。

置換先にはコンポーネント、コンポーネントセットの既定バリアント、インスタンスの参照元を利用できます。元の例の「複数選択可能」という説明に対して先頭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からノードを取得し直すため、登録後に削除されたものは失敗として数えます。ローカルの置換先が解決できない場合は、キーから公開コンポーネントの読み込みを試みます。公開状態と権限によって失敗する場合があります。

親インスタンスとその内部のインスタンスを両方登録すると、親の差し替えで子の構造が変わる場合があります。最初は同じ階層の対象に絞り、差し替え後に残りを登録し直してください。実行中の別操作は処理しないガードを入れていますが、共同編集者の変更まで停止するものではありません。

名前だけをそろえる場合は参照元との名前比較を使い分けます。

元記事の操作例

以下は元記事で掲載していた開発時の画面です。今回のコード改稿後に撮影した検証画像ではありません。現行の表示や変更結果は、複製したファイルで確認してください。

作成したプラグインの動作を検証している画面。
2025年08月の元記事の掲載例。改稿後コードの検証画像ではありません。
各インスタンスの差し替え処理を実装する流れを示した図。
2025年08月の元記事の掲載例。改稿後コードの検証画像ではありません。

検証範囲と関連ガイド

今回確認したのは掲載コードの構文と、該当する入力・例外処理のローカル模擬検証です。Figmaデスクトップアプリ上での動作確認とは区別しています。改稿後のコードと、元記事が参照していたGitHub上の版が一致するとは限りません。

開発環境と登録手順はFigmaプラグイン開発入門、公開はCommunityへの申請を参照してください。

サービス

Service

デザイン制作に関心がありましたら、ぜひ詳細をご覧ください。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

Figmaのおすすめ書籍

Figma用PCの選び方

周辺機器の優先度

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次