sb-figma — the Figma ↔ Storybook bridge
SkillMediaThe 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).
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the sb-figma skill
What this skill tells your AI
The instructions your AI receives, as published by strongeron/storybook-workbench in skills/sb-figma/SKILL.md and read by ahel’s review.
sb-figma moves an approved design into production Storybook (Jobs 1–2) and pushes the code's truth
back to Figma (Job 3, and opt-in writes). It uses the Figma MCP, but it is not the exploration skill.
Preconditions — check before the first call
| Job | Needs | Tools |
|---|---|---|
| Read tokens (Job 1, preferred) | edit access to the file; the remote server | use_figma (read-only script) |
| Read tokens (fallback) | any seat | get_variable_defs |
| Deliver a component (Job 2) | any seat | get_design_context, get_metadata, download_assets |
| Code Connect (Job 3) | Organization or Enterprise plan, published library components | get_code_connect_suggestions, get_context_for_code_connect, send_code_connect_mappings |
| Write to Figma (opt-in) | Full seat for an existing file (any seat can write to drafts); remote server; beta | use_figma, generate_figma_design |
- Prefer the remote server (
https://mcp.figma.com/mcp). The desktop server has no write, no code-to-canvas and no skills. - Rate limits are per plan and seat (Starter View/Collab seats get a handful of calls a month; Dev/Full seats
about 200 a day).
whoami,add_code_connect_mapandcreate_new_filedon't count. - A call fails with access, permission or rate-limit errors → call
whoami(plans + seats) and report what's missing instead of retrying. - Figma's own skills (
figma-use,figma-design-to-code,figma-code-connect,figma-generate-design) come with the Figma plugin; without it, read them from the MCP asskill://figma/<name>/SKILL.md. Tools that require one say so in their description — load it first and passskillNames.
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 tosb-explore. Seereferences/figma-token-sync.md.
Learn from each run — gaps and project rules
Every run starts here and ends here:
- Read the open gaps first:
node scripts/workbench-settings.mjs gaps. Each one says what a previous run couldn't do and how to fix it. Show them to the user before starting new work. - The scripts record gaps as they go in
.storybook/figma/gaps.json:collision(several Figma variables on one token),weak-match(named on the last word only),unmapped-name(Figma variables with no code token),unsupported-value(a colour syntax the parity can't read),unmapped-variant-value(a Figma variant value with no code value). A gap a later run no longer finds is marked resolved. - Record what only you see: a value with no token you had to ask about (
missing-token), a tool Figma refused (tool-refused):node scripts/workbench-settings.mjs add --kind missing-token --detail "icon #116932 has no token" --suggestion "…". - Fix a gap with a project rule, not a skill edit. Rules live in
.storybook/workbench.json(scaffold:node scripts/workbench-settings.mjs --init; schemareferences/workbench.schema.json); the project owns the file, so skill updates never overwrite it, and command-line flags still win:css— where the token CSS lives ·darkSelectors— what marks the dark themenameMap—"fg/error": "--color-error-text": beats name guessing, settles collisionsignore— Figma variables to leave out ("Size/*") ·variantValues—"status - success": "success"codeConnectLabel— default Code Connect label
- Something the rules can't fix (an unsupported colour syntax, a wrong result) →
report-issue.sh --gapsdrafts a sanitized issue with the open gaps as counts per kind — no names or values.
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_screenshotis 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.
# pipe each tool's output straight in (JSON, code text and XML are all stored as returned):
<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 2026-06-22; tool schemas re-checked 2026-10-01)
What the live Figma MCP actually returns — the scripts already handle these; know them so you don't fight the output:
get_variable_defsis 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 opaqueFont(family: …, size: …)string —pull-figma-variablesparses it to{family,size,weight,lineHeight,…}(a field may itself be a var-name ref likesize/text-lg). Shadows areEffect(…)→ theeffectfamily.classify()buckets by VALUE.get_metadatareturns 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 andget_design_context/get_metadatathe 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 examples use1:6965; every tool accepts both.capture-figmastores captures under the dash form so they're found either way. Every tool also needs thefileKeyfrom the URL (figma.com/design/<fileKey>/…; for a branch URL use the branch key). get_design_contextreturns code text plus a screenshot by default;get_metadatareturns XML. Onlyget_variable_defsand theuse_figmaread script return JSON.- 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).
The jobs (all read 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.
- Read the variables — every mode. Preferred: load Figma's
figma-useskill (or theskill://figma/figma-use/SKILL.mdresource), then calluse_figmawith the contents ofreferences/read-figma-variables.jsascodeandskillNames: "figma-use". It only reads: every local collection, every mode (Light, Dark, …), aliases resolved. Big system (output is capped near 20 kB): setONLYto one collection and call once per collection. Capture the result (Job 0). Fallback, when you can't runuse_figma(no edit access to the file):get_variable_defson the node that uses the tokens. It returns only the variables that node uses, in one mode — dark is not compared. Library variables the file consumes but doesn't own:get_libraries, thensearch_design_systemwith{"entity":"variable", …}. - Normalize —
node scripts/pull-figma-variables.mjs --from-mcp .storybook/figma/<tool>/<NODE>.json --out .storybook/figma-variables.json. It accepts either shape; the default mode fillscolor/spacing/type, other modes land inmodes. With no--from-mcpit reuses the last cache (the headless degrade path). - Build parity —
node scripts/build-token-parity.mjs --variables .storybook/figma-variables.json --css <token-css-glob> --out .storybook/figma-token-parity.json. The CSS is read per theme::rootis the default,.dark/[data-theme=dark]/ dark@mediablocks are the dark theme. Figma's default mode is compared with the default theme, a Figma "Dark" mode with the dark theme (modes.Darkin the output). OKLCH channel triplets resolve to hex;semantic/*maps to the project's--token, followingvar()alias chains. - Wire the foundation stories — pass
figmaParitytoTokenMatrix(it readsfigma-token-parity.json) to show drift in the color table's issue column (figma Δ,code #X vs figma #Yon hover). The fields are optional: a project with no Figma file renders as before. - Report drift in the sb-health shape: Figma value ≠ code value (OKLCH→hex tolerance for color, exact
for spacing/type), per mode; app-only roles (
--popover,--ring, …) are expected, not failures.
Job 2 — approved Figma component → production (sb-figma delivers; sb-stories authors)
- Size the delivery first — chunk a big board. A multi-artboard feature (a whole flow, a screen with many sections) blows past the MCP token budget. Split by artboard or section: deliver, validate and record one part, then the next. The Figma Inventory (step 8) unions stories across parts.
- Load Figma's design-to-code guidance.
get_design_contextnow requires it: use the/figma-design-to-codeskill (Figma plugin) or read theskill://figma/figma-design-to-code/SKILL.mdMCP resource, and passskillNames: "figma-design-to-code"("resource:figma-design-to-code"when read from the resource) on the call. SetclientFrameworks/clientLanguages(e.g.react,typescript). - Extract (into the Job 0 store). One
get_design_contextcall returns reference code, asset download URLs and a screenshot; capture it. If it comes back as metadata only (too large), fetch the child nodes listed in it instead of the parent.get_metadatagives the tree when you need structure alone. - Reuse before building. Code Connect-mapped components come back in the design context as the real
import — use them. Otherwise grep for an existing component covering the same concept and extend it
(guardrails §8 step 3);
search_design_systemfinds library components by name. - Assets. Use an exact match already in the project first (e.g. the same icon from the icon package the
app uses). Otherwise download each asset the way the
get_design_contextresponse describes, into the project's asset folder. No placeholders, no new icon package, no temporary Figma asset URLs left in code. - Build with approved tokens and primitives only — tokens, not magic numbers. A Figma value with no token → stop and ask (a missing-token task: Job 1 or the user). The reference code is a starting point to adapt, not to paste.
- Author the story with
sb-stories' conventions (materially different states only; a factory when 3+ stories share a shape), stamp the node-id in a top-of-file comment andparameters.design(Shared plumbing), then validate light / dark / mobile against the captured screenshot. - Record the delivery in the Figma Inventory — so the stories this feature created don't scatter
across the taxonomy. Idempotent; re-run per delivery, stories union by id:
Then ensure the root surface exists: scaffold once withpython3 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 createdscaffold-wrapper.sh --figma, drop thefigma-inventory.stories.tsxtemplate (titleFigma Inventory), add one export per feature (export const Hunts = { args: { feature: 'Hunts' } }), and pin it to the top in.storybook/previewviaoptions.storySort.order: ['Figma Inventory', '*']. TheFigmaInventorywrapper readsfigma-inventory.jsonand renders the index + each feature's board link + the stories it brought in.
Job 3 — Connect: Storybook → Figma (code → design)
Push the code's truth back so Figma Dev Mode shows your real components. Code Connect needs an Organization or Enterprise plan and published library components (see Preconditions). Follow Figma's order; every call goes through the Job-0 store:
- Check access —
whoami. No Org/Enterprise plan → stop and say so; Dev Mode won't show mappings. - Find the components —
get_code_connect_suggestionson the page or frame (excludeMappingPrompt: truefor a lean list of unmapped components). - Read each one —
get_context_for_code_connectper component node: its properties, variant options and descendants. Match it to the code component (a story withparameters.designfor that node is the strongest signal; else the component name). - Build the payload — write
comps.json(component,codeFile,figmaNode, optionalcomponentName,label,tokens,variantProperties,modes), then:node scripts/build-code-connect.mjs --components comps.json --file <FILE_KEY> \ --parity .storybook/figma-token-parity.json --usage .storybook/component-usage.json \ --out .storybook/code-connect.jsonsendis the exactsend_code_connect_mappingsinput (fileKey,nodeId,mappings[{nodeId, componentName, source, label}]);contextholds tokens, real props and variant → prop mappings for review;reverseParitylists components with no Figma node or no source, unsupported labels, drifted tokens. - Confirm, then send — show the user the mappings; on approval call
send_code_connect_mappingswithsendas-is. One mapping only:add_code_connect_mapworks too. Both write to Figma. - Prop-level snippets (optional) — a plain mapping shows the import and component. To map props (the
variant picker → code props), write a Code Connect template:
Component.figma.tsfor the@figma/code-connectCLI 2.x (template files are the only maintained format since 2.0;.figma.tsx+figma.connect()are migrate-only), or passtemplatein the mapping. Figma's/figma-code-connectskill covers templates;context[].propMappingsis the input.
Components with no Figma node (componentsWithoutNode) have nothing to connect to; creating them is a
write (below).
The loop closes: the parameters.design node-id added when delivering (Jobs 1–2) is what Job 3 reads to
connect back.
Writing to Figma (opt-in, off by default)
Only on the user's explicit request, and confirm before each write — it changes their file. Needs a Full seat for an existing file (see Preconditions).
- Default:
use_figma(loadfigma-usefirst,skillNames: "figma-use"). Build from the file's own components and variables (search_design_system), return every created node id, and record them. - Tokens → Figma variables (create a collection or update one, Light + Dark):
Paste one entry asnode scripts/build-figma-variables.mjs --css "src/**/*.css" --prefix "--color-bg,--color-text-" \ --collection "Code tokens" --out .storybook/figma-variables-push.jsonPAYLOADintoreferences/write-figma-variables.jsand run it withuse_figma—DRY = truefirst: it returns what it would create and update. Show that to the user, then run withDRY = false. It reuses a collection or variable with the same name and overwrites values; it deletes nothing. Check with Job 1: reading the collection back and running parity should show zero drift. generate_figma_designonly to capture a rendered page the first time — e.g. a story rendered in isolation:http://localhost:6006/iframe.html?id=<story-id>&viewMode=story. It makes a pixel copy; refine it withuse_figmaagainst the library components. Updating a page already captured →use_figma.- After a write, the new node id goes back into the story's
parameters.design, so Jobs 2–3 see it.
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(preservecp, 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 URL → the
fileKeyevery tool needs; a node URL for Jobs 2–3 (and for theget_variable_defsfallback: a frame that uses the tokens). 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. - Plan and seat for Job 3 and writes — see Preconditions (
whoami).
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.
Signals
- GitHub stars
- 37
- Last commit
- Oct 2026
ahel review
S4info
community integration, published by strongeron, not figma
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Item type
- skill
- Key
sb-figma- Source
- github.com/strongeron/storybook-workbench
github.com/strongeron/storybook-workbench
Related picks
Skill · thedaviddias
The pick for JavaScriptmodern-javascript-patterns
Skill · wshobson
The pick for JavaScriptpython-performance-optimization
Skill · wshobson
The pick for Pythonpython-pro
Skill · jeffallan
The pick for Pythonsetup-ts-deep-modules
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScript