--- name: prototype-to-figma description: > Converts a working Claude Code prototype into a structured Figma design file — exploding each interaction flow into separate frames, using the target file's design system components, and annotating interaction details natively in Figma. Use this skill whenever the user asks to turn a prototype into Figma frames, generate design specs from a prototype, create a Figma handoff from code, explode a prototype into states, or make a prototype reviewable by cross-functional partners. Also trigger when the user mentions "prototype to Figma", "design review from prototype", "async feedback on prototype", "break prototype into flows", "Figma specs from code", or wants to make a working prototype legible to non-technical stakeholders. Even if the user just says "put this in Figma" or "make this reviewable" in the context of a prototype, use this skill. --- # Prototype → Figma This skill takes a working Claude Code prototype and produces a structured Figma file that cross-functional partners can review asynchronously. The output has two equal goals: 1. **True 1:1 visual parity** — layout, colors, spacing, typography, content, and icons match the *rendered* prototype exactly, because the base is produced by a **headless render → serialize** (Rule 0), not a hand-reconstruction. Runs entirely in the terminal — no visible browser or tab. 2. **A design-system-native, reviewable result** — captured elements are reconciled to DS component instances / bound variables / text styles wherever the DS is published to Figma (Rule 2), and every state transition is annotated on its node. **This skill works across all Figma MCP clients.** The output format adapts to what your client supports — see [Client Compatibility](#client-compatibility) below. --- ## Five non-negotiable rules ### Rule 0: Capture headlessly (render → serialize), never open a visible browser; then reconcile to the DS **True 1:1 requires *rendering* the code — but rendering does NOT require a *visible* browser.** Use a **headless** browser (Playwright) so the whole run stays in the terminal: no window, no tab. That is the point for the target user — designers working exclusively in code who must not be pulled out of the IDE. Three options; only one is right: - ❌ **Read source and hand-build (no render).** An LLM re-deriving a browser's layout/render engine element-by-element is lossy, incomplete, and buggy on any non-trivial page. It is **not 1:1** — it only looks faithful on small, static UIs, and it drifts badly on real ones (collapsed grids, missing elements, half-built SVGs). Do not pretend this is 1:1. - ❌ **Visible capture** — `open "#figmacapture=…"`, the in-app preview, or the GUI Figma extension. Correct fidelity, but it pops windows/tabs and drags the designer out of the terminal (and the bare extension gives no DS instances). - ✅ **Headless capture.** A headless browser renders the running app in the background and serializes the DOM **1:1 straight to Figma** (data goes browser→Figma, never through the model — so it's fast and cheap). The model then adds the design-system layer. Terminal-native, 1:1, invisible. **"Headless" = a separate Chrome process launched with `--headless`: no window, no tab, nothing on screen.** It does NOT attach to the designer's open browser and does NOT require Chrome to be *open* — only *installed*. It spins up invisibly, renders the localhost page in memory, serializes to Figma, and exits. The designer never leaves the terminal and sees nothing happen in their browser. **The capture (headless, terminal-only):** 1. Ensure the dev server is running — start it in the background if needed. Determine the route URLs from the codebase. 2. Launch a headless browser from the terminal and run the `generate_figma_design` capture script (navigate to the LOCAL dev URL → strip CSP → inject `capture.js` → `window.figma.captureForDesign( { captureId, endpoint, selector:'body' })`). **Prefer the machine's already-installed Chrome** (`puppeteer-core`, or Playwright with `channel:'chrome'`, or raw CDP) so there's **no download and no MCP** — Chrome need only be *installed*, not open. **Only if no Chrome/Chromium is found**, fall back to a one-time bundled-Chromium download (`npx playwright install chromium` / full `puppeteer`). **Never** launch a *new visible instance*, never attach to the user's live browser, never use `open "#figmacapture=…"` or the in-app preview — all of those put a window/tab on screen. 3. Poll `generate_figma_design` (fileKey + captureId) until `completed`. You get a fully-layered 1:1 base (frames / text / vectors) with tokens bound (`bindVariables=true`). **Then reconcile to the DS + annotate (`use_figma`, Rule 2):** swap repeated captured elements for DS component instances, bind variables / text styles **where the DS is actually published to Figma**, and attach Dev Mode annotations + flow arrows. This is the cheap, surgical layer — it touches a handful of components, not the whole page. **Requirements / honesty:** needs (a) a **headless browser** — ideally the machine's installed Chrome driven headless (no download, no MCP required); a bundled Chromium download is the fallback — and (b) a runnable dev server. No Playwright *MCP* is required; a headless Chrome runtime is. If there's genuinely no browser at all, **say so and stop** — do not fall back to a visible tab, and do not hand-build from source and call it 1:1 (it won't be). If the design system isn't published to Figma as components/variables, reconciliation is limited to what exists — flag that DS gap, don't swap in a foreign library's components. ### Rule 1: Never create new Figma components **Do not** call `figma.createComponent()` or `figma.createComponentSet()`. These pollute the design system with components that don't belong there. When a prototype element has **no component of any kind** in the DS (see Rule 2 for the bar): build it from primitives (`figma.createFrame()`, `figma.createRectangle()`, `figma.createText()`), add a **DS Drift annotation** explaining what was missing, and list it in the Phase 6 summary. ### Rule 2: Always use a DS component / variable / style when one exists — the capture does NOT relax this The capture (Rule 0) gives parity; this rule makes the file design-system-native. **It applies as you reconcile each captured element** — the captured node tells you *where*, *how big*, and *how it looks*, but the structure should come from the DS wherever a match exists. Walk the captured frame and reconcile *every* element to the linked library: 1. **Components — always instance, never shadow.** If a captured element matches a DS component, **replace it with an instance** and override props (fill, text, size) to match the captured pixels. If the component exists but lacks the exact variant, still instance it + override, and add a DS Drift annotation. Only when there is **no** matching component does the element stay a primitive — flagged DS Drift. Repeated elements especially: 60 badges = 60 instances, never 60 frames. A frame full of primitives that shadow real components is a failed run even if the pixels are perfect. 2. **Color variables — always bind.** Every fill/stroke whose value matches a DS color variable MUST bind that variable. Never leave a raw hex where a color variable exists. 3. **Number variables — always bind.** Every spacing, gap, padding, corner radius, border width, size, and font-size that matches a DS number/dimension variable MUST bind it. Never leave a raw number where a number variable exists. 4. **Text styles — always apply.** Body/label/heading text binds to the library text style, not raw `fontSize`/`fontName`. The rule in one line: **prefer the component / color variable / number variable / text style over any literal whenever a match exists.** Raw hex, ad-hoc numbers, and shadow-primitives are drift — not parity — even when they look right. Phase 2 discovers the components, variables, and styles; Phase 4 swaps and binds them onto the captured base. ### Rule 3: Never omit a prototype element Every visible element must appear in the Figma output — DS instance or primitive. Skipping elements because you couldn't find a DS match is the most common cause of incomplete outputs. ### Rule 4: Annotations must be attached to nodes, and must be flow-critical only **Annotations are what make this output useful for review.** A frame with no annotations is just a static image. But annotating every tap target creates noise that buries the actual flow. Annotate using `node.annotations = [...]` on the actual layer — this surfaces in Figma Dev Mode on the correct element. **Never create floating text boxes or separate annotation frames** as a substitute. If the native API fails, fall back to `figma-patterns.md` Section 11. Two categories only: - **Interaction** (blue) — state transitions, primary CTAs, grouped controls - **DS Drift** (orange) — missing DS component, missing variant, or unbound color/spacing **Annotate (Interaction):** - **Frame-level** — one annotation per frame: what state this is and what the primary action does / where it goes next - **State-advancing CTAs** — the action(s) that trigger a state change (e.g., Submit, Confirm, Create goal, Move to completed) - **Grouped controls** — annotate the container once, not each item (annotate the amount picker row, not each chip; annotate the story pill rail, not each pill) - **Non-obvious tap targets** — elements that open a new state but don't look like buttons (e.g., a goal card that opens a detail sheet) **Do not annotate:** - Close / cancel / dismiss buttons - Nav bar buttons and tab items - Secondary utility actions (settings, prototype controls) - Individual chips or pills within a grouped picker - Backdrops and overlays - Decorative elements **Target: 2–4 Interaction annotations per frame.** A reviewer should be able to read all annotations on a frame in under 30 seconds and understand the complete flow. Every element with a DS Drift note from Phase 2 must have a DS Drift annotation. --- ## Client Compatibility | Tier | Inspect | Write | Code Connect | Output | |---|---|---|---|---| | **Full** | ✅ | ✅ | ✅ | Figma file with DS components, native annotations, Code Connect links | | **Write** | ✅ or ❌ | ✅ | ❌ | Figma file with native annotations | | **Inspect-only** | ✅ | ❌ | ❌ | Prototype Spec Document (markdown) | | **None** | ❌ | ❌ | ❌ | Prototype Spec Document from code analysis alone | | Client | Inspect | Write | Code Connect | |---|---|---|---| | Claude Code / Desktop / Cursor / VS Code / Copilot CLI / Augment / Factory | ✅ | ✅ | ✅ | | Android Studio / Gemini CLI / Kiro / Amazon Q / Openhands | ✅ | ❌ | ❌ | | Replit | ❌ | ✅ | ❌ | --- ## Figma MCP tools **Inspect:** `get_design_context` (primary), `get_metadata` (structure overview), `search_design_system` (components + variables), `get_variable_defs` (exact token values), `get_screenshot` (only if user explicitly requests a visual preview). **Capture (primary path — Rule 0):** `generate_figma_design` driven by a **headless** browser — ideally the machine's **installed Chrome** run headless (`puppeteer-core` / Playwright `channel:'chrome'` / CDP; no download, no MCP), bundled Chromium only as a fallback — against the local dev server. Renders + serializes 1:1 to Figma with no visible window/tab. NEVER the `open`-a-tab flow or the in-app preview (visible), and never attach to the user's open browser. **Write:** `use_figma` (reconcile captured nodes to DS instances + variables, annotate, assemble the flow), `whoami`, `create_new_file`. **Code Connect:** `get_code_connect_map`, `get_code_connect_suggestions`, `get_context_for_code_connect`, `send_code_connect_mappings`. --- ## The workflow ### Phase 0: Resolve file and detect capabilities **File:** Extract `fileKey` from a user-provided URL (`figma.com/design/:fileKey/...`). If no URL is given, call `whoami` then `create_new_file` — never block on a missing URL. **Capabilities:** Attempt `get_metadata` to confirm Inspect tools. Check tool list for `use_figma` (Write) and `get_code_connect_map` (Code Connect). Announce what you have. **Route:** Write tools available → Phases 1–6. Write tools unavailable → Phases 1–3, then [Prototype Spec Document](#prototype-spec-document--inspect-only-output). --- ### Phase 1a: Analyze the prototype Read all prototype source files before touching Figma. **Component inventory** — for every UI component, record: name, props/variants in use, visual structure (what's inside it), and whether it is interactive. This list is the completeness checklist for Phase 4. **Interaction flows** — for each flow, map: trigger → state change → before/after states → branching paths (success / error / edge cases). Include state count. **Layout structure** — note what's fixed vs. scrollable, what persists across states (nav, header), and the viewport dimensions. For the phone/device frame: if the prototype wraps screens in a `.phone`, `.device-frame`, or similar shell, **extract only the content dimensions** and use those for Figma frame size — do not recreate the shell as a wrapper frame in Figma. **Source read (for inventory + structure, NOT for pixel values)** — read the CSS modules, Tailwind config, and inline styles to understand each element's structure, variants, and which are interactive. Do **not** treat source CSS values as the pixel source of truth: responsive/ computed CSS (grid `fr`, `clamp()`, `min/max`, `vw`, transforms, theme scopes) resolves to formulas, not pixels. The pixel source of truth is the **computed measurement** from the running app (Rule 0 / Phase 4). Use source reading only to know *what* to measure and match to the DS. --- ### Phase 1b: Present scope, get selection, confirm **Skip if the prototype has ≤ 2 flows.** Proceed directly to Phase 2 with all flows. For 3+ flows, present a categorized flow list with state counts, ask "which flows and how much detail (happy path / all states)?", and wait for a reply before proceeding. Confirm the interpretation explicitly before starting Phase 2. --- ### Phase 2: Discovery pass — map to the design system before drawing anything Do a full discovery pass against the linked library **first**, then map every element. Do not start building until this table exists. The default is *instance*, not primitive (Rule 2). **2a. Enumerate the library once, up front.** Before searching per component, pull the catalog so you know what exists: - **Components:** `search_design_system(fileKey, includeComponents=true)` with broad queries, plus category sweeps ("input", "navigation", "feedback", "card", "badge", "avatar", "table") to surface the full set — not just the names your code happens to use. - **Variables:** `search_design_system(..., includeVariables=true)` and `get_variable_defs` for color, spacing, radius, and **border-width** tokens. Record the keys. - **Text styles:** capture the library's typography styles (name → font, size, weight, line height) from `search_design_system` / `get_variable_defs`. These are what body copy, labels, and headings must bind to — not hardcoded hex + ad-hoc `fontSize`. **2b. Match each inventory element** (try in order before declaring "no match"): 1. `search_design_system(fileKey, query="ComponentName", includeComponents=true)` 2. Alternate names: Alert/Banner/Toast/Notification, Dropdown/Select/Menu, Tag/Chip/Badge, Nav/Sidebar/TabBar, TextField/Input, TextArea/Multiline, Avatar/UserPill, etc. 3. Visual category: "input", "navigation", "feedback", "card" 4. Parent component coverage (e.g., "Card" covers `Card.Header`) **For found components:** call `get_context_for_code_connect` to get exact variant prop names before setting properties in Phase 4. **When a component exists but the exact variant does not:** do **not** drop to a primitive. Instantiate the component and override the differing property per-instance (fill, text, size), then add a DS Drift annotation for the missing variant. Only elements with *no* matching component at all become primitives. **Build the mapping table — every row needs a build approach and drift note:** | Code component | DS match | Key | Build approach | Drift note | |---|---|---|---|---| | `