sb-figma — Figma → production Storybook delivery
sb-figma owns the DELIVER stage of the Figma→Storybook lifecycle: an approved design becomes
production Storybook. It is Figma-aware (uses the Figma MCP), but it is not the exploration skill.
The lifecycle line (load this first). Undecided / trying options → sb-explore (Lab sandbox,
iterate against a Figma node) → sb-ship (graduate the Lab experiment). Already approved in Figma →
sb-figma (deliver direct to prod). Both touch Figma MCP; the difference is exploration vs delivery.
If the user is still deciding, hand off to sb-explore. See references/figma-token-sync.md.
Job 0 — capture the MCP output (always, before anything else)
Force the NATIVE structured tools — never work from a screenshot. A screenshot is pixels; it cannot
give you variables, styles, or component props. The full picture comes ONLY from the native MCP tools, and
you must pull all three categories before building or connecting:
- Variables →
get_variable_defs (the token values: color/spacing/type/effect, resolved).
- Components + styles →
get_design_context (the reference code, applied styles, props/variants).
- Structure →
get_metadata (the node tree; on truncation, drill to child node-ids — never give up at
the parent).
get_screenshot is visual reference only — for an eyeball diff after you've built from the structured
data. NEVER read tokens, props, or layout off a screenshot. If a tool returns "nothing selected" on a page
id, drill to a concrete component node (a page is not a layer).
Scripts can't call the Figma MCP, and an MCP result lives only in the agent's context — ephemeral, gone
on the next session, absent headless. So every Figma MCP call you make, persist it through the universal
store before using it. This is what makes the pipeline reproducible and iterable.
# any JSON-returning tool — pipe its output straight in:
<get_variable_defs output> | node scripts/capture-figma.mjs --tool get_variable_defs --file <FILE> --node <NODE> --from-mcp -
<get_design_context output> | node scripts/capture-figma.mjs --tool get_design_context --file <FILE> --node <NODE> --from-mcp -
<get_metadata output> | node scripts/capture-figma.mjs --tool get_metadata --file <FILE> --node <NODE> --from-mcp -
<get_code_connect_map out> | node scripts/capture-figma.mjs --tool get_code_connect_map --file <FILE> --node <NODE> --from-mcp -
# get_screenshot returns an image — save it, then register the file:
node scripts/capture-figma.mjs --tool get_screenshot --file <FILE> --node <NODE> --image /tmp/frame.png
# see the whole inventory (degrade / iterate):
node scripts/capture-figma.mjs --list
Store layout: .storybook/figma/manifest.json + .storybook/figma/<tool>/<node>.json (images keep their
ext). Re-capturing a (tool,node) overwrites — diff against git to see what moved in Figma. Downstream steps
read the store, never re-call MCP.
MCP realities (field-verified against the Detections file, 2026-06-22)
What the live Figma MCP actually returns — the scripts already handle these; know them so you don't fight the output:
get_variable_defs is a FLAT { "name": "value" } map (not nested DTCG, not an array). Colors come already
resolved to hex ("semantic/background":"#fbfcfc"), incl. 8-digit alpha ("#e4e5e580"). Numbers are bare
strings ("spacing-2":"8", "wght/semibold":"650"). Typography is an opaque Font(family: …, size: …)
string — pull-figma-variables parses it to {family,size,weight,lineHeight,…} (a field may itself be a
var-name ref like size/text-lg). Shadows are Effect(…) → the effect family. classify() buckets by VALUE.
get_metadata returns XML and TRUNCATES on large frames (a table view blew past the token limit). When it
truncates: read the child node ids from the partial XML and get_design_context/get_metadata the sub-node,
not the parent. Capture each sub-node to the store so you never re-fetch.
- Node-ids: the URL uses
1-6965, the API uses 1:6965. capture-figma canonicalizes to dash form, so a
capture is found whichever way it's passed next. Always pass the node from the URL as-is.
- Persistence + screenshot↔node linking are solved by Job 0 — the store keeps every output with its node-id in
the manifest, so you don't hand-cross-reference screenshots or re-call MCP (both were real friction before).
Two jobs (both consume the Job-0 store)
Job 1 — foundation tokens → Foundations/Colors|Tokens|Type (sb-figma writes these directly)
No other skill maps Figma variables to code tokens, so sb-figma owns the foundation parity end to end.
- Normalize the captured variables — after Job 0 stored
get_variable_defs, point the normalizer at the
stored file: node scripts/pull-figma-variables.mjs --from-mcp .storybook/figma/get_variable_defs/<NODE>.json --out .storybook/figma-variables.json
(with no --from-mcp, it reuses the last --out cache — the headless degrade path). Captures colors,
spacing, and type (wght/text/leading).
- Build parity —
node scripts/build-token-parity.mjs --variables .storybook/figma-variables.json --css <token-css-glob> --out .storybook/figma-token-parity.json.
Resolves OKLCH channel triplets → hex, maps each Figma semantic/* → the project's --token
(following var() alias chains), and emits a { token: { figmaVar, figmaHex, mapsTo, drift } } map for all
three families.
Pass figmaParity to TokenMatrix (it reads figma-token-parity.json itself) to surface drift
right in the color table's issue column — figma Δ with code #X vs figma #Y on hover — so design↔code
parity lives next to the token, not only in docs/figma-token-parity.md.
- Wire the foundation stories —
Colors.stories.tsx / Tokens.stories.tsx (+ a Type group) read
figma-token-parity.json and pass figmaVar/figmaHex into TokenMatrix rows (and the spacing/type
sections). The fields are optional — a project with no Figma file renders exactly as before.
- Drift — the parity map flags Figma-value ≠ code-computed-value (OKLCH→hex tolerance for color, exact
for spacing/type) and lists app-only roles (
--popover, --ring, …) as expected, not failures.
Report in the sb-health shape.
Job 2 — approved Figma component → production (sb-figma delivers; sb-stories authors)
- Extract (via Job 0 store) — capture
get_design_context for the node (fall back to get_metadata then
a sub-node fetch if truncated) + get_screenshot, then read them back from .storybook/figma/. The
variant list + node-id come from the stored design-context, so re-runs don't re-hit MCP.
- Audit before adding — grep for an existing component covering the same concept; extend it rather than
duplicate (mirrors guardrails §8 step 3).
- Build the component with approved tokens/primitives only — tokens, not magic numbers; if a Figma
value has no token, stop and ask (it's a missing-token task, route to Job 1 or the user).
- Author the story by following
sb-stories' conventions — materially-different states only (no
Cartesian), a factory when 3+ stories share a shape. Do not reinvent CSF3 rules; load sb-stories.
- Stamp + embed — node-id in a top-of-file comment, and
parameters.design (see Shared plumbing).
- Validate — light / dark / mobile, screenshot-vs-implementation parity.
- Size the delivery FIRST — chunk a big board into parts. A multi-artboard feature (a whole flow,
a screen with many sections) blows past the MCP token budget and produces a "too long, what's the
status?" mega-pass. Split by artboard / section: deliver + validate + record one part, then the
next. The Figma Inventory (step 7) accretes stories across parts (union by id), so an incremental
delivery is first-class, not a workaround. Don't attempt the whole board in one turn.
- Record the delivery in the Figma Inventory — so the stories this feature created don't just scatter
across the taxonomy. Run the recorder (idempotent; re-run per delivery, stories union by id):
"${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/scripts/record-figma-delivery.py" . \
--figma-url "<board url>" [--feature "<Name>"] --spec-url "<spec node url>" --node-ids 101-9717 \
--description "<one line>" \
--story "Hunts/Hunt Packs:hunts-hunt-packs--default:component" # repeat per story you created
Then ensure the root surface exists: scaffold once with scaffold-wrapper.sh --figma, drop the
figma-inventory.stories.tsx template (title Figma Inventory), add one export per feature
(export const Hunts = { args: { feature: 'Hunts' } }), and pin it to the top in .storybook/preview
via options.storySort.order: ['Figma Inventory', '*']. The FigmaInventory wrapper reads
figma-inventory.json and renders the index + each feature's board link + the stories it brought in.
(Spec: docs/specs/2026-06-23-figma-feature-inventory.md.)
Job 3 — Connect: Storybook → Figma (the reverse direction, code→design)
The reverse of Jobs 1–2: push the code's truth back to Figma so Dev Mode shows your real components. Driven
from the Storybook side, fed by context the bundle already has — no new scanners.
- Gather — for each component that has a story AND a Figma node (the
parameters.design node-id from
deliverable #8): its used tokens (from figma-token-parity.json), real prop values (from
component-usage.json), variants (from the CSF3 stories), and — from get_metadata — its Figma variant
properties ({type:[root,nest-1,nest-2,nest-3]}) and modes ([Light,Dark]). Assemble a comps.json
(variantProperties + modes + optional propAliases to rename a Figma prop, e.g. type→depth).
build-code-connect maps each variant property → a code prop with its value enum, and carries the modes as
the theme dimension — so the Code Connect mapping drives a variant-rich component from Figma's variant picker.
- Build the payload —
build-code-connect.mjs --components comps.json --parity .storybook/figma-token-parity.json [--usage .storybook/component-usage.json] --out .storybook/code-connect.json. It enriches each token with its
figmaVar + value, canonicalizes node-ids, and emits a reverse-parity report: components with no Figma
node, drifted tokens, tokens not in the parity map.
- Push (the agent, via MCP) —
send_code_connect_mappings / add_code_connect_map with the
mappings[] from code-connect.json. This is the primary, low-risk path — it makes Figma Dev Mode
show the real code/props/tokens. Gate the push (it writes to Figma): confirm before sending.
- Generate (opt-in, OFF by default) — for a component in
componentsWithoutNode, generate_figma_design
from the code + story (foundation tokens supplying real values). It creates design artifacts, so only on
explicit user intent.
The loop closes: the parameters.design node-id added when delivering (Jobs 1–2) is what Job 3 reads to
connect back. Capture any get_code_connect_map / suggestions to the Job-0 store like every MCP call.
Shared plumbing — the Figma design embed (Docs)
Every story sb-figma touches gets the design source preserved on its catalog page:
parameters: { design: { type: 'figma', url: 'https://figma.com/file/<FILE_ID>?node-id=<NODE_ID>' } }
This is @storybook/addon-designs (the "Design" tab — the same mechanism sb-explore uses for frames). Plus
a node-id stamp + link in parameters.docs.description. sb-explore and sb-stories can reuse this snippet
whenever the node-id is known.
Boundaries — never duplicate a sibling's verb
- Exploration / "try a v2" / undecided →
sb-explore (Lab). sb-figma is for approved designs only.
- Graduating a Lab experiment →
sb-ship (preserve cp, rewrite callsites). sb-figma delivers from
Figma, not from a /explore/ experiment.
- Documenting an existing code component (no Figma) →
sb-stories. sb-figma calls sb-stories' rules to
author; it never reimplements them.
- Code-internal token health / orphans →
sb-health / sb-inventory (Figma-free). sb-figma adds the
design↔code parity those can't see.
Inputs the agent must resolve first
- Figma file id + variables node id (the variables/styles spec frame) — ask if not pasted.
- Token CSS path(s) — where
--token: <value> declarations live (e.g. src/styles/**/*.css).
- Is the design approved? If the user is still iterating → stop, route to
sb-explore.
Next
Run Job 1 (tokens) first so components built in Job 2 consume real, parity-checked tokens. Append progress to
.storybook/audit/status.md for clean resume. Full call sequence + the OKLCH→hex notes:
references/figma-token-sync.md.
1---2name: sb-figma3description: The Figma↔Storybook bridge, both directions, via the native Figma MCP. design→code: map foundation tokens (color/spacing/type) from Figma variables with design↔code parity + drift, and deliver approved Figma components (extract → build → embed), authoring stories via sb-stories' rules. code→design: build Code Connect mappings from components + stories + token parity + usage so Figma Dev Mode shows the real code. Use for 'sync my Figma tokens', 'map Figma variables to my design system', 'deliver this approved Figma design', 'check design↔code token parity', 'connect my components to Figma / code connect'. NOT for prototyping/iterating an undecided design (that's sb-explore).4license: MIT5---67# sb-figma — Figma → production Storybook delivery89`sb-figma` owns the **DELIVER** stage of the Figma→Storybook lifecycle: an **approved** design becomes10production Storybook. It is Figma-aware (uses the Figma MCP), but it is **not** the exploration skill.1112> **The lifecycle line (load this first).** *Undecided / trying options* → **`sb-explore`** (Lab sandbox,13> iterate against a Figma node) → **`sb-ship`** (graduate the Lab experiment). *Already approved in Figma* →14> **`sb-figma`** (deliver direct to prod). Both touch Figma MCP; the difference is **exploration vs delivery**.15> If the user is still deciding, hand off to `sb-explore`. See `references/figma-token-sync.md`.1617## Job 0 — capture the MCP output (always, before anything else)1819> **Force the NATIVE structured tools — never work from a screenshot.** A screenshot is pixels; it cannot20> give you variables, styles, or component props. The full picture comes ONLY from the native MCP tools, and21> you must pull all three categories before building or connecting:22> - **Variables** → `get_variable_defs` (the token values: color/spacing/type/effect, resolved).23> - **Components + styles** → `get_design_context` (the reference code, applied styles, props/variants).24> - **Structure** → `get_metadata` (the node tree; on truncation, drill to child node-ids — never give up at25> the parent).26> `get_screenshot` is **visual reference only** — for an eyeball diff after you've built from the structured27> data. NEVER read tokens, props, or layout off a screenshot. If a tool returns "nothing selected" on a page28> id, drill to a concrete component node (a page is not a layer).2930Scripts can't call the Figma MCP, and an MCP result lives only in the agent's context — ephemeral, gone31on the next session, absent headless. So **every Figma MCP call you make, persist it** through the universal32store before using it. This is what makes the pipeline reproducible and iterable.3334```bash35# any JSON-returning tool — pipe its output straight in:36<get_variable_defs output> | node scripts/capture-figma.mjs --tool get_variable_defs --file <FILE> --node <NODE> --from-mcp -37<get_design_context output> | node scripts/capture-figma.mjs --tool get_design_context --file <FILE> --node <NODE> --from-mcp -38<get_metadata output> | node scripts/capture-figma.mjs --tool get_metadata --file <FILE> --node <NODE> --from-mcp -39<get_code_connect_map out> | node scripts/capture-figma.mjs --tool get_code_connect_map --file <FILE> --node <NODE> --from-mcp -40# get_screenshot returns an image — save it, then register the file:41node scripts/capture-figma.mjs --tool get_screenshot --file <FILE> --node <NODE> --image /tmp/frame.png42# see the whole inventory (degrade / iterate):43node scripts/capture-figma.mjs --list44```4546Store layout: `.storybook/figma/manifest.json` + `.storybook/figma/<tool>/<node>.json` (images keep their47ext). Re-capturing a (tool,node) overwrites — diff against git to see what moved in Figma. **Downstream steps48read the store, never re-call MCP.**4950## MCP realities (field-verified against the Detections file, 2026-06-22)5152What the live Figma MCP actually returns — the scripts already handle these; know them so you don't fight the output:53- **`get_variable_defs` is a FLAT `{ "name": "value" }` map** (not nested DTCG, not an array). Colors come **already54 resolved to hex** (`"semantic/background":"#fbfcfc"`), incl. 8-digit alpha (`"#e4e5e580"`). Numbers are bare55 strings (`"spacing-2":"8"`, `"wght/semibold":"650"`). **Typography is an opaque `Font(family: …, size: …)`56 string** — `pull-figma-variables` parses it to `{family,size,weight,lineHeight,…}` (a field may itself be a57 var-name ref like `size/text-lg`). Shadows are `Effect(…)` → the `effect` family. `classify()` buckets by VALUE.58- **`get_metadata` returns XML and TRUNCATES on large frames** (a table view blew past the token limit). When it59 truncates: read the child node ids from the partial XML and `get_design_context`/`get_metadata` the **sub-node**,60 not the parent. Capture each sub-node to the store so you never re-fetch.61- **Node-ids: the URL uses `1-6965`, the API uses `1:6965`.** `capture-figma` canonicalizes to dash form, so a62 capture is found whichever way it's passed next. Always pass the node from the URL as-is.63- **Persistence + screenshot↔node linking are solved by Job 0** — the store keeps every output with its node-id in64 the manifest, so you don't hand-cross-reference screenshots or re-call MCP (both were real friction before).6566## Two jobs (both consume the Job-0 store)6768### Job 1 — foundation tokens → `Foundations/Colors|Tokens|Type` (sb-figma writes these directly)6970No other skill maps Figma *variables* to code tokens, so sb-figma owns the foundation parity end to end.71721. **Normalize the captured variables** — after Job 0 stored `get_variable_defs`, point the normalizer at the73 stored file: `node scripts/pull-figma-variables.mjs --from-mcp .storybook/figma/get_variable_defs/<NODE>.json --out .storybook/figma-variables.json`74 (with no `--from-mcp`, it reuses the last `--out` cache — the headless degrade path). Captures **colors,75 spacing, and type** (`wght`/`text`/`leading`).762. **Build parity** — `node scripts/build-token-parity.mjs --variables .storybook/figma-variables.json --css <token-css-glob> --out .storybook/figma-token-parity.json`.77 Resolves OKLCH channel triplets → hex, maps each Figma `semantic/*` → the project's `--token`78 (following `var()` alias chains), and emits a `{ token: { figmaVar, figmaHex, mapsTo, drift } }` map for all79 three families.80 Pass `figmaParity` to `TokenMatrix` (it reads `figma-token-parity.json` itself) to surface **drift**81 right in the color table's issue column — `figma Δ` with `code #X vs figma #Y` on hover — so design↔code82 parity lives next to the token, not only in `docs/figma-token-parity.md`.833. **Wire** the foundation stories — `Colors.stories.tsx` / `Tokens.stories.tsx` (+ a `Type` group) read84 `figma-token-parity.json` and pass `figmaVar`/`figmaHex` into `TokenMatrix` rows (and the spacing/type85 sections). The fields are optional — a project with no Figma file renders exactly as before.864. **Drift** — the parity map flags Figma-value ≠ code-computed-value (OKLCH→hex tolerance for color, exact87 for spacing/type) and lists **app-only roles** (`--popover`, `--ring`, …) as *expected*, not failures.88 Report in the sb-health shape.8990### Job 2 — approved Figma component → production (sb-figma delivers; sb-stories authors)91921. **Extract (via Job 0 store)** — capture `get_design_context` for the node (fall back to `get_metadata` then93 a sub-node fetch if truncated) + `get_screenshot`, then read them back from `.storybook/figma/`. The94 variant list + node-id come from the stored design-context, so re-runs don't re-hit MCP.952. **Audit before adding** — grep for an existing component covering the same concept; extend it rather than96 duplicate (mirrors guardrails §8 step 3).973. **Build** the component with approved tokens/primitives only — **tokens, not magic numbers**; if a Figma98 value has no token, **stop and ask** (it's a missing-token task, route to Job 1 or the user).994. **Author the story by following `sb-stories`' conventions** — materially-different states only (no100 Cartesian), a factory when 3+ stories share a shape. Do **not** reinvent CSF3 rules; load `sb-stories`.1015. **Stamp + embed** — node-id in a top-of-file comment, and `parameters.design` (see Shared plumbing).1026. **Validate** — light / dark / mobile, screenshot-vs-implementation parity.1030. **Size the delivery FIRST — chunk a big board into parts.** A multi-artboard feature (a whole flow,104 a screen with many sections) blows past the MCP token budget and produces a "too long, what's the105 status?" mega-pass. Split by **artboard / section**: deliver + validate + record one part, then the106 next. The Figma Inventory (step 7) accretes stories across parts (union by id), so an incremental107 delivery is first-class, not a workaround. Don't attempt the whole board in one turn.1087. **Record the delivery in the Figma Inventory** — so the stories this feature created don't just scatter109 across the taxonomy. Run the recorder (idempotent; re-run per delivery, stories union by id):110 ```bash111 "${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/scripts/record-figma-delivery.py" . \112 --figma-url "<board url>" [--feature "<Name>"] --spec-url "<spec node url>" --node-ids 101-9717 \113 --description "<one line>" \114 --story "Hunts/Hunt Packs:hunts-hunt-packs--default:component" # repeat per story you created115 ```116 Then ensure the root surface exists: scaffold once with `scaffold-wrapper.sh --figma`, drop the117 `figma-inventory.stories.tsx` template (title `Figma Inventory`), add **one export per feature**118 (`export const Hunts = { args: { feature: 'Hunts' } }`), and pin it to the top in `.storybook/preview`119 via `options.storySort.order: ['Figma Inventory', '*']`. The `FigmaInventory` wrapper reads120 `figma-inventory.json` and renders the index + each feature's board link + the stories it brought in.121 (Spec: `docs/specs/2026-06-23-figma-feature-inventory.md`.)122123### Job 3 — Connect: Storybook → Figma (the reverse direction, code→design)124125The reverse of Jobs 1–2: push the code's truth back to Figma so Dev Mode shows your real components. Driven126from the Storybook side, fed by context the bundle already has — **no new scanners**.1271281. **Gather** — for each component that has a story AND a Figma node (the `parameters.design` node-id from129 deliverable #8): its used tokens (from `figma-token-parity.json`), real prop values (from130 `component-usage.json`), variants (from the CSF3 stories), and — from `get_metadata` — its **Figma variant131 properties** (`{type:[root,nest-1,nest-2,nest-3]}`) and **modes** (`[Light,Dark]`). Assemble a `comps.json`132 (`variantProperties` + `modes` + optional `propAliases` to rename a Figma prop, e.g. `type→depth`).133 `build-code-connect` maps each variant property → a code prop with its value enum, and carries the modes as134 the theme dimension — so the Code Connect mapping drives a variant-rich component from Figma's variant picker.1352. **Build the payload** — `build-code-connect.mjs --components comps.json --parity .storybook/figma-token-parity.json136 [--usage .storybook/component-usage.json] --out .storybook/code-connect.json`. It enriches each token with its137 `figmaVar` + value, canonicalizes node-ids, and emits a **reverse-parity report**: components with no Figma138 node, drifted tokens, tokens not in the parity map.1393. **Push (the agent, via MCP)** — `send_code_connect_mappings` / `add_code_connect_map` with the140 `mappings[]` from `code-connect.json`. This is the **primary, low-risk** path — it makes Figma Dev Mode141 show the real code/props/tokens. **Gate the push** (it writes to Figma): confirm before sending.1424. **Generate (opt-in, OFF by default)** — for a component in `componentsWithoutNode`, `generate_figma_design`143 from the code + story (foundation tokens supplying real values). It *creates* design artifacts, so only on144 explicit user intent.145146The loop closes: the `parameters.design` node-id added when *delivering* (Jobs 1–2) is what Job 3 reads to147*connect back*. Capture any `get_code_connect_map` / suggestions to the Job-0 store like every MCP call.148149## Shared plumbing — the Figma design embed (Docs)150151Every story sb-figma touches gets the design source preserved on its catalog page:152153```ts154parameters: { design: { type: 'figma', url: 'https://figma.com/file/<FILE_ID>?node-id=<NODE_ID>' } }155```156157This is `@storybook/addon-designs` (the "Design" tab — the same mechanism `sb-explore` uses for frames). Plus158a node-id stamp + link in `parameters.docs.description`. `sb-explore` and `sb-stories` can reuse this snippet159whenever the node-id is known.160161## Boundaries — never duplicate a sibling's verb162163- **Exploration / "try a v2" / undecided** → `sb-explore` (Lab). sb-figma is for *approved* designs only.164- **Graduating a Lab experiment** → `sb-ship` (preserve `cp`, rewrite callsites). sb-figma delivers from165 *Figma*, not from a `/explore/` experiment.166- **Documenting an existing code component (no Figma)** → `sb-stories`. sb-figma *calls* sb-stories' rules to167 author; it never reimplements them.168- **Code-internal token health / orphans** → `sb-health` / `sb-inventory` (Figma-free). sb-figma adds the169 *design↔code* parity those can't see.170171## Inputs the agent must resolve first172173- **Figma file id** + **variables node id** (the variables/styles spec frame) — ask if not pasted.174- **Token CSS path(s)** — where `--token: <value>` declarations live (e.g. `src/styles/**/*.css`).175- **Is the design approved?** If the user is still iterating → stop, route to `sb-explore`.176177## Next178179Run Job 1 (tokens) first so components built in Job 2 consume real, parity-checked tokens. Append progress to180`.storybook/audit/status.md` for clean resume. Full call sequence + the OKLCH→hex notes:181`references/figma-token-sync.md`.