0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Figma to Code を閉ループにする — 「目視でだいたい合ってる」を禁止する測定ファースト実装

0
Last updated at Posted at 2026-07-24

本記事はシリーズ「自己学習する開発環境」の第4回(最終回)です。

  1. AIが同じミスを二度としない開発環境(全体像)
  2. 設計書1枚から実装・レビュー・PRまで
  3. AIレビューの指摘を資産化する
  4. Figma to Code を閉ループにする(本記事)

用語メモ

  • Storybook: UI コンポーネントを1つずつ単独で表示・確認できる開発ツール。表示パターンの定義を「Story」と呼ぶ
  • バリアント: 同じコンポーネントの状態違い(選択中・完了・エラーなど)
  • 画素計測: スクリーンショットをピクセル単位で測り、色や間隔を数値として取り出すこと
  • デザイントークン: 色やサイズに名前を付けて一元管理する仕組み(例: primary-500

Figma MCP1 でコンポーネントを実装させると、一見それっぽいものが出てきます。しかしよく見ると、7種類あったはずの余白(16/40/32/44/24px…)が全部 gap-6 に均されている。アイコンが塗りつぶしのはずなのに線画になっている。色がデザイントークン1段ぶんだけ違う。

そして最悪なのは、本人(AI)が「Figma と一致しています」と自信満々に報告してくることです。

左のデザイン参照と右の Storybook 実装を並置。一見似ているが、タイトル色の取り違え(#000000→#9AA0A6)とフォントサイズの肥大(16px→22px)、説明→日付の余白膨張(8px→32px)に赤枠と実測値の注釈が付いている
一見似ているが、測ると差分が出る。左はデザイン参照(再現作図)、右は実装(Storybook)。タイトルの色・サイズと余白のズレが、実測で数字として確定する
試行錯誤の末にたどり着いた結論はこうです。Figma to Code の精度問題の本質は、生成の精度ではなく検証の精度にあります。「目視でだいたい合ってる」を「一致」と報告することが、すべての誤差の出どころです。だから、実装 → 撮影 → 画素計測 → 修正の閉ループを組み、「測っていない項目は『一致』と書けない」というルールを敷きました。本記事では、その仕組みを全部紹介します。

1. 従来の Figma to Code が抱えてきた問題

まず、Figma to Code がこれまで何につまずいてきたのかを整理しておきます。

第1世代: プラグインによるコード出力。 Figma のレイヤー構造がそのまま DOM になり、既存のコンポーネント規約とは無縁のコードが出てくる。動くデモにはなっても、プロダクトのコードベースには入れられませんでした。

第2世代: Figma MCP + コーディングエージェント。 エージェントが Figma からデザインの数値情報(色コード・サイズ・余白など、実装に使う値)を取得し、既存の規約・既存コンポーネントに沿って実装できるようになりました。ここで「コードとして使える」水準には達します。しかし精度問題が残ります。

筆者のチームの実測(後述の「失敗頻度ランキング」)では、頻発する誤りは次の順でした。

  1. 間隔の均し(最多): 異なる7種の区切りを単一の gap で潰す。二重 padding。箱内の配置(justify)の見落とし
  2. テキスト色の取り違え: 隣接要素の色を拾う。トークン表で隣り合う色の混同(red-2(#FF0033) と red-3(#FE5C5C) など。名前は1段違いでも色は別物)
  3. タイポグラフィの role 潰し: ラベル 12px Bold と値 14px Bold を一律の variant にしてしまう
  4. アイコンのバリアント: 線画/塗りの違いを「一致」と誤判定
  5. 状態の取りこぼし: 代表バリアントだけ実装し、残りの状態を落とす

いずれも「全体をパッと見ると合っている」タイプの誤りです。つまり目視の全体比較では原理的に検出できません

もうひとつ、運用上の問題があります。トークンコストです。Figma MCP の出力は巨大で、スクリーンショットは画像トークンの塊、Storybook の起動ログはノイズの山。これを全部メインセッションに積むと、修正ループを回すほどコンテキストが汚れ、コストが跳ね上がります。

2. 4ステップ閉ループとコンテキスト隔離

そこで、フローを4ステップの閉ループにし、重い処理をサブエージェントに隔離しました。

(色分け: 青=メインセッションの工程 / 青緑=サブエージェントに隔離した工程)

ステップ 担当 メインに返すもの
Step 1: Figma取得 サブエージェント デザイン数値の要約(色コード・サイズ・余白・フォント・バリアント一覧)+ 保存パス
Step 2: 実装 メイン 実装そのもの(判断が要る高難度部分)
Step 3: Storybook撮影 サブエージェント 撮影ファイルパスだけ
Step 4: 比較 サブエージェント スペック突き合わせ表 + 差分リストだけ

第2回で紹介した指揮者パターンの応用です。こだわったのは次の2点です。

画像を Read するのは比較サブエージェントの中だけ。 メインセッションには一切画像を載せません。Figma からの取得もスクリーンショットを除外したテキスト値中心で行い、比較用 PNG は curl でファイル保存するだけ。画像トークンは、比較を担当する別コンテキストに閉じ込めます。

PNG の再利用ルール。 Figma 側の PNG はデザインが変わらない限り有効なので、セッション中は1回だけ取得して使い回します。一方 Storybook 側はコード変更のたびに再撮影。これで修正ループの2周目以降が大幅に安くなります。

ただし、この再利用には後述する落とし穴があり、実際に事故りました(4章)。

ハンズオン①: 呼び出しから Step 1 の出力まで

ここからは、ItemCard というカード部品を例に1周してみます。呼び出しは、コンポーネント名と Figma URL(バリアントが複数あれば全部)を渡すだけです。

/figma-to-code ItemCard を以下の Figma パターンと比較して

- 選択済み(ピンク背景): https://www.figma.com/design/xxxx/?node-id=1234-5678&m=dev
- 未選択(白背景):       https://www.figma.com/design/xxxx/?node-id=1234-5679&m=dev

Step 1 を担当する figma-fetch エージェントの定義は、こんな指示で始まります(実物の抜粋・マスキング済み)。

---
name: figma-fetch
description: Figma MCPからデザインの実装値を取得し要約して返す。トークン削減のため
  軽量モデルで動作し、巨大なMCP出力を消化して要約値とファイルパスだけを呼び出し元に返す。
model: sonnet
---

## 手順
### 1. 実装値の取得(テキスト中心)
mcp__figma__get_design_context(fileKey, nodeId, excludeScreenshot: true)

- excludeScreenshot: true を必ず付けてスクリーンショットを省き、トークンを節約する
- 参考コードから 色(hex)、サイズ(px)、間隔(gap/padding)、border-radius、
  font(size/weight/line-height)を抽出する
- ボーダー/区切り線は丁寧に拾う(要約ミス多発ポイント):
  - 線から線までの距離(border-to-border)を必ず算出して報告する。リストが
    flex gap-N で区切り線も子要素の場合、gap は線の両側に効くため
    border間 = gap + 項目高 + gap(例: 20+20+20=60px)
- 色・スタイルはコード要約だけで断定しない。アイコン色はコードが currentColor で
  実色が出ないことがある。不明なときは「コード上は◯、実表示要確認」と両論併記する

「gap は線の両側に効く」のような注意書きが妙に具体的なのは、全部過去のミスの跡です(4章)。なお excludeScreenshot のような引数名は執筆時点の環境でのものです。Figma MCP はツール名・引数の更新が早いので、手元のバージョンのドキュメントで読み替えてください。このエージェントがメインに返してくる「デザイン数値の要約」は、こんな形になります(サンプル)。

## ItemCard デザイン数値の要約(選択済みパターン)
- サイズ: カード 328×96px / radius 12px
- 背景: #FFF1F4(token: pink-1)/ 枠線 1px #FE5C5C(token: red-3)
- 間隔: 外周 padding 16px / タイトル→説明 gap 8px / 説明→メタ行 gap 12px
- フォント: タイトル 14px/700/lh20、説明 12px/400/lh17
- バリアント: 選択済み / 未選択 / 完了(グレー)の3種
- 保存PNG: temp/.../figma-selected.png ほか3枚(親フレーム込み)

ItemCard の Storybook スクリーンショットに、外周 padding 16px・アイコン↔本文 gap 8px・タイトル→説明 gap・説明→日付 gap 12px・スターボックス radius 12px と枠線 1px を a〜f の記号で注釈した画像
Step1 の要約の各値が、カードのどこを指すかの対応(値は実装 CSS 基準)。Figma 要約の各項目に 1:1 で対応させ、測って裏取りする
実装(Step 2)は、この要約とプロジェクトの規約スキルをもとにメインセッションが行います。ここで素の HTML/CSS を書くのではなく、プロジェクトのデザインシステムの既存コンポーネント(テキスト・ボタン・レイアウトなどの共通部品)に寄せて実装するのもポイントです。たとえばテキストは、フォントサイズや太さを CSS に直書きせず、共通の AppTypographyvariant に委ねます。

// ✗ 素のスタイル直書き: Figma のどのテキストスタイルに対応するのか不明。
//    ラベルと値を同じ指定に潰しても気づけない
<span style={{ fontSize: 14, fontWeight: 700, lineHeight: '20px' }}>タイトル</span>
<span style={{ fontSize: 12, fontWeight: 400, lineHeight: '17px' }}>説明</span>

// ✓ variant に委譲: variant 名が Figma のテキストスタイルと 1:1 で対応する
<AppTypography variant="title-14-bold">タイトル</AppTypography>
<AppTypography variant="body-12-regular">説明</AppTypography>

AppTypographyvariant は、Figma 側で定義されているテキストスタイル(タイポグラフィの variant)と名前レベルで対応するよう設計されています。だから実装は「Figma のこのスタイル → コードのこの variant」という引き当てになり、サイズ・太さ・行間を個別に転記する必要がありません。裏を返すと、1章で挙げた失敗「タイポグラフィの role 潰し」——ラベルと値を一律の variant にしてしまうミス——も、variant 名の食い違いとして表に出やすくなります(variant 名は説明用のサンプルです)。既存規約を無視した DOM を吐いていた第1世代のプラグイン出力が解けなかったのが、まさにこの部分でした。とはいえ、ここまではまだ普通の Figma to Code です。本領は次の「測って検証する」から。

3. 測定ファースト — 「一致」と書ける条件を定義する

ここが本記事の核心です。比較サブエージェントの大原則は、次の一文に尽きます。

目視の印象で判定しない。測って、数字で突き合わせる。

計測は共有スクリプトで、毎回同じ結果になるように行う

比較エージェントは、Python の共有スクリプトで画素を計測します。

python3 measure.py size <img>                        # 画像サイズ(サイズの基準合わせ用)
python3 measure.py hex <img> <x> <y>                 # 指定座標周辺の最頻色hex
python3 measure.py bands <img> <x0> <y0> <x1> <y1>   # 要素の帯検出とgap実測
python3 measure.py zoom <img> ... <out>              # 4x拡大クロップ(小型要素・アイコン用)

エージェントに毎回スニペットを書き捨てさせず、共有スクリプトに固定しているのもポイントです。計測パラメータ(色サンプリングの半径・帯検出の許容差)が毎回変わると、計測結果が再現しなくなるからです。スクリプト自体は Pillow + numpy の素朴な画像処理で、hex は指定座標の周辺で最も多い色を数え、bands は帯状の範囲で「背景と明るさが違う行」を検出しているだけ(全体で150行ほど)。座標は、撮った PNG を開いて「測りたい要素を縦に貫く細い帯」の左上・右下を読み取って渡します。

計測前には、サイズの基準合わせ(スケール正規化)を挟みます。Figma の PNG と Storybook の撮影 PNG は解像度が違うので、両方に写っている基準要素(カード幅など)から scale 係数を出し、距離をすべて Figma の px 単位に直してから比較します。

ハンズオン②: ItemCard の間隔と色を実際に測る

先ほどの ItemCard で「タイトル→説明→メタ行」の縦の間隔を測ってみます。テキストを縦に貫く細い帯(ストリップ)の座標を指定して bands を実行すると、コンテンツの帯と帯の間の gap が数値で出ます。

$ python3 measure.py bands storybook-selected.png 40 10 60 180
bg_luma=255 bands=3 axis=v
band[0]: 16..36 (h=20)
band[1]: 44..61 (h=17) gap_from_prev=8
band[2]: 85..99 (h=14) gap_from_prev=24

1行目の bg_luma は背景の明るさ、axis=v は縦方向の走査を表します。band が3つ = テキスト行が3つ。gap_from_prev がそのまま行間の実測値です。Step 1 の要約は「タイトル→説明 gap 8px / 説明→メタ行 gap 12px」でした。実測の 8 は一致。しかし2つ目は 24 — 12px のはずの間隔が倍になっています。実装が2箇所の異なる間隔を単一の gap で均してしまった、冒頭で挙げた典型ミスが、ここで初めて数字として姿を現すわけです。目視では「なんとなく間延びしてる?」程度にしか見えません。

色も同じ要領です。文字の線の中心の座標を指定して、Figma 側と実装側の両方を測ります。

$ python3 measure.py hex figma-selected.png 52 26      # Figma側: タイトル文字の芯
#1A1A1A
$ python3 measure.py hex storybook-selected.png 50 25  # 実装側: 同じ位置
#1A1A1A

ItemCard のスクリーンショットに、bands の縦ストリップ範囲(青い破線の矩形)と hex のサンプリング座標(文字の芯に打った赤点)を図示し、右に実測結果を並べた注釈画像
どこを測っているか。bands は縦長のストリップで行間を、hex は文字の芯に打った点で色を測る。輪郭のぼかしを拾わないよう線の中心を狙う
こうして測った値で、次の突き合わせ表を1行ずつ埋めていきます。

スペック突き合わせ表 — 「ほぼ一致」の禁止

比較エージェントの主な成果物は、スペック突き合わせ表(デザインの値と実装の値を1行ずつ並べて比べる表)です。

要素 項目 Figma値 実装実測 判定 確信度
カード外周 padding 16px 16px 一致
タイトル→説明 gap 8px 8px 一致 高(bands 実測)
説明→メタ行 gap 12px 24px 差分 高(bands 実測)
タイトル #1A1A1A #1A1A1A 一致 高(hex 実測)
説明 font-size / weight 12px / 400 14px / 400 差分
ステータスアイコン バリアント 塗り 線画 差分 高(4x拡大で確認)
ボタン 高さ 48px 要裏取り 低(実測不能・再撮影要)

ルールは3つです。

  1. 判定は「一致 / 差分 / 要裏取り」の3値のみ。「ほぼ一致」は禁止。 曖昧なら拡大して再計測するか「要裏取り」にする
  2. 「一致」と書けるのは、Figma値と実装実測の両方が数字(px / カラーコード / バリアント名)で埋まった行だけ。 数字を書けない項目は「未確認」であって「一致」ではない
  3. 値が取れない行も省略しない。「要 fetch 再依頼」と書いて返す

過去の誤判定(gap の一律共通化・色の取り違え・塗り/線画の誤り)は、例外なく「目視でだいたい合っている」を「一致」と報告したことが原因でした。だから報告のフォーマット自体から「だいたい」を排除しています。

計測の勘所

実運用で溜まったノウハウをいくつか。

  • 間隔は「境界ごとに1行」。 まとめて「gap OK」と書かせません。実装が単一の flex gap-N で複数の境界を潰していると、表の複数行が同じ値になるので検出できます
  • テキスト色は文字の線の中心を測る。 輪郭のぼかし部分(アンチエイリアス)を拾うと、実際には無い差分が出ます
  • 40px 以下の小型要素・アイコンは、両側とも 4x 拡大してから判定。 全体 PNG の目視では線画/塗りの区別がつきません
  • Figma と CSS の「枠線の付き方」の違いを補正する。 Figma の stroke は(デフォルトの Inside 設定では)枠の内側に、CSS の border は padding の外に付きます。枠付きで高さ未指定の箱は、元の値どおりの padding を入れると 2px ズレる — こうした仕組み上必ず生じるズレは、ルール化して吸収します

measure.py の実行結果ターミナル。size で 720x428、bands で3つの帯(band[0..2])と gap_from_prev=18 / 21、hex でタイトル #000000・最終評価日 #404040 が出力されている
measure.py の実行例。bands が帯(テキスト行)を検出し gap_from_prev に行間の実測値を出す。hex は指定座標の色を返す(この撮影サンプルでの実測値)
星アイコンを4x拡大した比較画像。左は面が塗られた「塗り」(チェック済み)、右は輪郭だけの「線画」(未チェック)で、両者の違いが明確に判別できる
4x 拡大クロップ。全体 PNG の目視では潰れて「一致」と誤判定されやすい線画/塗りの差が、拡大すると明確に判別できる

4. figma 版の自己学習ループ — 失敗頻度ランキング

第3回で、レビュー指摘をログに貯めてレビューエージェントのチェック配分を変える仕組みを紹介しました。同じ思想が、この視覚比較にも入っています。

比較エージェントの定義には失敗頻度ランキングがあり、過去の実ミスをカテゴリ別にカウントしています。

## 失敗頻度ランキング(この順に計測を重点配分する)

| 優先 | カテゴリ | 回数 |
|---|---|---|
| 1 | 間隔(gapの一律共通化・二重padding・箱内の配置 等) | 9 |
| 2 | テキスト色(隣接要素との取り違え・近似トークンの1段差 等) | 7 |
| 3 | タイポグラフィ(role別のサイズ/太さの差) | 7 |
| 4 | アイコン/イラスト(塗り/線画バリアント・グリフ形状 等) | 6 |
| 5 | 構造(要素順序・カード分割・バリアント網羅) | 3 |

(上位5カテゴリの抜粋です。ほかに box-model 差など少数のカテゴリがあります)

そしてメンテナンスルールがこうです。

新しい実ミスが発生したら、新しい項目(bullet)を足すのではなく、該当カテゴリの「回数」を +1 する(代表例の差し替えは可)。新カテゴリの追加は、既存カテゴリに収まらない新種のみ。

チェックリストの肥大化は流し読みの温床です。項目数を固定して重みだけを動かすことで、リストを短く保ったまま、チェックの手間をよく起きるミスに集中させ続けられます。「回数が多いカテゴリから先に・細かく計測する」— レビューエージェントの頻出違反ランキングと同型の、視覚版の自己学習ループです。なおレビュー版と違い、こちらは件数が少なく並行更新もまず起きないため、追記専用ログは挟まずランキング表を直接 +1 する運用にしています(増えてきたら同じログ方式に移行する予定です)。

実際の事故がルールになった例

このスキルのルールの多くは、実際のインシデントから生まれています。3つ紹介します。

「PNG が残っていても Step 1 の『デザイン数値の要約』は毎回必要」。 PNG を再利用できるからと Step 1 を丸ごとスキップしたら、比較エージェントは突き合わせ表の「Figma値」列を画素実測で埋めるしかなくなりました。その結果、Figma 側 PNG に紛れ込んでいた描画ノイズ——このときは Web フォントの読み込みが間に合わず、別のフォントで描画された状態のまま保存されていた——まで「Figma の値」として扱い、もともと正しかった実装を差分として誤報告したのです。突き合わせ表の Figma 値の元データはあくまで fetch の要約であり、PNG は見た目の確認用。この区別を落とすと計測ファーストが逆に牙をむきます。

「画素実測から起こした値を『Figma 上の本来の値』として扱わない」。 アイコンが 24px か 28px か、画素では確定できないケースがあります。実測のまま実装すると「値は合うが理由を説明できない」箇所が増えるので、確定できない値は fetch で Figma 上の定義値を引き直します。逆に、その定義値をそれが答えている範囲を超えて隣の要素に流用してもいけません(これも実際にやらかしました)。

「バリアント網羅ゲート」(全状態がそろっているかの事前チェック)。 代表的な状態だけ実装して、残りのバリアントを取りこぼす事故がありました。以来、実装に入る前に「Figma のバリアント一覧 N 種 ⇔ mock / Story で N 種カバー」を突き合わせ、一致を確認してから視覚ループへ進むゲートを置いています。

5. 運用の勘所

Story 化の判断軸を持つ。「ページ配下を厳密比較したい」と言われても、配下の全コンポーネントを Story 化してはいけません。個別比較する価値があるのは「Figma に 1:1 対応する、見た目の表示だけを担当する末端の部品」だけ。複合コンポーネントや hook に依存するもの、レイアウト用の frame はページ単位の Story でカバーします。雑に全部 Story 化すると、時間だけ溶けます。

スコープを明示する。 このループが扱うのは「Figma のフレーム幅での静止状態」の比較のみです。レスポンシブ(複数ブレークポイント)・ダークモード・hover などのインタラクション状態・アニメーションは対象外として明記しています。なんでも比較しようとすると閉ループが閉じなくなります。スコープを絞ったから閉ループにできた、というのは設計判断として強調しておきたい点です。なお対象コンポーネントも、Storybook を持つフロントエンドに限定しています(筆者のチームでは Storybook 未導入の管理画面は対象外です)。

開発フローへの組み込み。 このループは単独でも呼べますが、第2回の orchestrate の FE トラックに組み込まれています。設計書に Figma URL を書いておくと、実装ステップでこの視覚一致ループがコンポーネント単位で自動的に織り込まれます。

差分ゼロになった最終状態。左のデザイン参照と右の Storybook 実装が一致し、右の突き合わせ表は padding・gap・radius・タイトル色すべてが「一致」になっている
差分ゼロの最終状態。突き合わせ表の全行が px / カラーコードで埋まって「一致」になっている(「ほぼ一致」は無し)

まとめ — シリーズ全体の回収

Figma to Code の精度向上とは、生成を賢くすることではなく、検証を定量化し、その検証自体が学習するようにすることでした。

  • 比較の判定を3値(一致 / 差分 / 要裏取り)にし、「ほぼ一致」を禁止しているか
  • 「一致」に実測値(px / カラーコード)の根拠を要求しているか
  • 画像・巨大出力をメインコンテキストから隔離しているか
  • 過去の見た目のミスをカテゴリ×回数で管理し、チェックの重点配分に使っているか
  • 全バリアントがそろっているかを実装前に確認しているか
  • ループのスコープ外(レスポンシブ等)を明示しているか

最後に、シリーズ全体を少し引いて眺めてみます。第2回の開発フロー、第3回の環境学習、そして本記事の視覚一致。扱っている対象はバラバラなのに、やっていることはきれいに同型です。

  1. 閉じる — 生成して終わりにせず、検証と修正で輪を閉じる
  2. 測る — 検証を目視や自己申告ではなく、計測と検証コマンドに紐づける
  3. 学びを書き戻す — ミスをその場で直して終わらせず、環境(ログ・ランキング・DoD・ルール)に書き戻す

この3点セットを、自分の仕事のあらゆる場所に仕掛けていくこと。それが、私たちが実運用から学んだ「ループエンジニアリング」の実践です。

参考リンク

  1. MCP = Model Context Protocol。AI エージェントが外部ツール・データへ接続するための標準規格。Figma MCP はこれを通じて Figma のデザインデータを取得できるようにするサーバーです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?