Rebuilds a Figma design as code so that input, validation, state, and branching actually work in a single HTML file. Not a click-through screenshot demo — something you press to check the UX and get a feel for the implementation spec. The pipeline runs as settle scope by interview, read Figma faithfully (structure, exact labels, visual tokens), build as vanilla single-file HTML, then verify real behavior in a browser. Triggers - "/fig:proto", "make a working prototype", "make this design actu...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add byjunyoung/claude-product-skills --skill proto --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Proto?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/byjunyoung-proto)More formats (shields.io, HTML) on the badges page.
---
name: proto
description: Rebuilds a Figma design as code so that input, validation, state, and branching actually work in a single HTML file. Not a click-through screenshot demo — something you press to check the UX and get a feel for the implementation spec. The pipeline runs as settle scope by interview, read Figma faithfully (structure, exact labels, visual tokens), build as vanilla single-file HTML, then verify real behavior in a browser. Triggers - "/fig:proto", "make a working prototype", "make this design actually clickable", "동작 프로토타입 만들어", "프로토타이핑 해줘", "실제로 눌러보고 입력되게".
allowed-tools: AskUserQuestion, Bash, Read, Write, Edit, mcp__plugin_figma_figma__get_design_context, mcp__plugin_figma_figma__get_screenshot, mcp__plugin_figma_figma__get_metadata, mcp__plugin_figma_figma__use_figma, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__computer, mcp__claude-in-chrome__browser_batch, mcp__claude-in-chrome__read_console_messages
---
# fig:proto — Figma design → working prototype (rebuilt in code)
**Part of a plugin.** The scripts this skill runs ship beside it under `${CLAUDE_PLUGIN_ROOT}`. If that path does not resolve, this file was installed on its own — stop and say the plugin itself is needed (`claude plugin install fig@byjunyoung`), rather than improvising what the scripts do.
**Rebuilds in code** what is drawn in Figma, producing a single HTML prototype where form input, validation, state changes, and dynamic branching actually work. Not a click-through demo of screenshots but something for **checking behaviour and getting a feel for the implementation spec** — you type a real value and watch the save land in the list.
**The premise:** because it is rebuilt in code, **the visuals are an approximation** (not pixel-perfect against the design) — in exchange, it actually runs. Build-less, dependency-less, single HTML, so it opens on a double click. For a presentation demo where pixel fidelity comes first, this is the wrong approach and a screenshot-based one is right (no dedicated skill for that; capture manually).
**How far "approximation" goes — layout, colour, and spacing. Not assets.** Drawings, illustrations, icons, photographs — **anything that already exists in the design as an image is inlined from the original rather than redrawn in code** (step 3). A drawing imitated in code is always worse than the original, and above all **it ends up inventing behaviour the design never had** — the moment a still image gets redrawn as vectors and made to "move with the value", the prototype hands an engineer a spec that does not exist.
**Prerequisites**: always load the `figma:figma-use` skill before calling `use_figma`. **Zero writes** — the Figma file is only read.
## When to invoke
- "a working prototype", "make it actually pressable", "make input and validation work", "make it behave the way it is drawn in Figma"
- "prototype this" (in a context where input and state are expected to work)
- An explicit "/fig:proto"
## When NOT to invoke
- A pixel-exact demo that only clicks between screens (no input needed) → screenshot-based (no dedicated skill; manual)
- Porting into production code (a frontend repo) → `/fig:code`
- Just seeing the frame list and structure → `/fig:read`
- Auditing the structure and flow of what is drawn → `/fig:lint`
## Inputs
- `figma_url` (required): figma.com/design/:fileKey/...?node-id=... — the target page or screen. Ask if it is missing.
- Target flow (optional): which screen or flow is the centre. Without it, settled in the step-1 interview.
## Where the deliverable goes / publishing
The path and the publish target are set by the `tools` section of `figma-conventions.yaml` — read with `python3 ${CLAUDE_PLUGIN_ROOT}/_common/scripts/lib/resolve-config.py`.
- The default is a local single HTML file. The location is `tools.proto_output_dir` and the filename is `<name>-prototype.html`. Being one file, it opens on a double click.
- Publishing (optional) follows `tools.proto_publish` (repo, account, visibility). When it is `null`, stop at local and do not offer to publish. Never hardcode a repo or an account in this document.
---
## Procedure
### 1. Interview — what counts as done
Fidelity is fixed for this skill: **behavioural**. Confirm the rest in sequence (AskUserQuestion, a recommendation attached to each, one at a time). Share the full list of what needs settling first so the scale is visible, then:
- **Scope**: every screen, or one core flow (create → list → detail, say). A working prototype gets a better token-to-value ratio from **concentrating on the core flow** than from covering everything — usually weighted toward the form, where the input logic lives.
- **Data**: fixed mock-ups, or state held for the session (what you make shows up in the list). For a creation flow, holding state is effectively mandatory.
- **Reference width**: desktop or mobile (as Figma has it).
- **Tech and deployment**: vanilla single HTML (recommended) versus React and the like; local only versus published.
Pin the definition of done in verifiable form (which cards, branches, and states have to work), and for several stages, share the plan before starting.
### 2. Read Figma accurately (read-only — no confirmation needed, explore freely)
Moving it "as drawn" means securing the labels, the structure, and the visuals alike. The target frame id comes from the URL's `node-id` (that frame or page), or from `get_metadata` (the page nodeId) enumerating sections down to screen frames. Then combine four sources:
- **Structure** — `get_design_context` on the target screen. It gives the frame tree, field placement, and the component skeleton. But **large frames come back sparse with text masked as `text`**, so the real labels are missing (layout only).
- **Exact labels** — so a read-only `use_figma` script collects `characters`, absolute coordinates, and hidden status for every TEXT in the frame in one pass (far cheaper in tokens than repeating get_design_context for each sublayer). Map fields to labels by coordinate, and read **required markers (*), defaults, branching hints, and helper text out of the hidden text**. **Harvest the example rows and values** of lists and detail views here too, as the prototype's seed data.
```js
// Collect characters + absolute coordinates for every TEXT in a frame (read-only, return only)
const page = figma.root.children.find(p => p.id === "<PAGE_ID>");
await figma.setCurrentPageAsync(page);
const g = (n,k)=>{try{return n[k]}catch(e){return undefined}}; // phantom-safe
const collect = async (id) => {
const root = await figma.getNodeByIdAsync(id), out = [];
const walk = (n) => { if(!n) return;
if (n.type==="TEXT") { const ch=g(n,"characters");
if (ch && ch.trim()) { const b=n.absoluteBoundingBox;
out.push({ y:b?Math.round(b.y):0, x:b?Math.round(b.x):0, t:ch, h:!n.visible?1:0 }); } }
const kids=g(n,"children"); if(kids) for(const k of kids) walk(k); };
walk(root); out.sort((a,b)=> a.y-b.y || a.x-b.x); return out;
};
return { form: await collect("<FRAME_ID>") }; // for several frames, bundle them into one script's return
```
- **Visual tokens** — do not shoot every screen; take **one or two representatives only** with `get_screenshot` (maxDimension set for legibility) → save with `curl` → `Read`. Pull nothing but colour, spacing, corner radius, component look, and button styles (the labels are already secured by the TEXT walk). One list-type and one form-type screen is usually enough.
- **The image-asset list** — scan for **nodes carrying an IMAGE fill** the same way as the TEXT walk, and build a list. This becomes the settled list of "fetch, do not draw". Check alongside it whether that node has **overlays laid on as children** (markers, numbers, badges) — if it does, exporting the node whole brings the overlays along exactly as they are.
```js
// Scan for IMAGE fill nodes (read-only) — the result is the asset list to inline in step 3
const g=(n,k)=>{try{return n[k]}catch(e){return undefined}};
const scan = async (frameId) => {
const root = await figma.getNodeByIdAsync(frameId), hits = [];
const walk = n => { if(!n) return;
const fills = g(n,"fills");
if (Array.isArray(fills) && fills.some(f => f.type === "IMAGE"))
hits.push({ id:n.id, name:n.name, type:n.type,
w:Math.round(n.width), h:Math.round(n.height),
kids:(g(n,"children")||[]).length }); // kids>0 = has overlays
for (const k of (g(n,"children")||[])) walk(k); };
walk(root); return hits;
};
return await scan("<FRAME_ID>");
```
### 3. Build the single working HTML (vanilla, no build, no dependencies)
Make it **a self-contained single file** — CSS, JS, **and images all inlined**, with no external font, CDN, or image **links** (system font stack). That is what lets it open on a `file://` double click and run in the user's hands with no server for verification. **"No links" is not "no assets"** — putting images in as `data:` URIs rather than links is how the principle is kept.
- **Inline the image assets (the default — ask before dropping any)**: export the IMAGE nodes scanned in step 2 as PNG with `get_screenshot` → save with `curl` → **substitute in as base64 `data:` URIs**. In practice each is tens of KB and several together are a few hundred, no burden on a single HTML file (meaning there is almost never a size reason to drop one — only when it really is large do you flag it and let the user decide).
- Write **only a placeholder** such as `__IMG_<NAME>__` in the HTML and substitute the base64 in with Bash and Python. Writing long base64 directly through an editor tool wastes whole tokens.
- The export scale is capped at the original size (a small node blown up with maxDimension does not gain resolution). For sharpness, pick a larger node in Figma or check the original scale.
- Shooting with `contentsOnly: true` drops unrelated overlapping elements while **keeping that node's child overlays intact**.
```bash
# placeholder → base64 data URI substitution (write __IMG_A__ into the HTML first, then run)
python3 - <<'PY'
import base64, pathlib
html = pathlib.Path('<output.html>')
s = html.read_text()
for token, f in (('__IMG_A__','a.png'), ('__IMG_B__','b.png')):
if token not in s: raise SystemExit(f'placeholder missing: {token}')
s = s.replace(token, 'data:image/png;base64,' + base64.b64encode(pathlib.Path(f).read_bytes()).decode())
html.write_text(s)
print('substituted', round(html.stat().st_size/1024), 'KB')
PY
```
- **What to draw in code versus what to fetch**: draw in code only what **actually has to change** with a value or a state (a gauge that fills, a cell that lights up by state, an element that rotates in response to input — **and only where that variation is drawn in the design**). A picture fixed as a single image in the design is fetched as it is. When the call is unclear, settle it with "is this element drawn differently in this screen's state-variant frames?" — if not, it is static.
- **Design tokens**: the colours, spacing, and radii pulled from the screenshots go into `:root` CSS variables. Do not scatter hardcoded values; route through tokens. Match the original mood (light, admin tone, and so on).
- **Layout and routing**: an SPA — `render()` off `state.view`, with **event delegation** (one click, input, and change handler on `#app`, branching on `data-action` / `data-field`). Full re-render only on **structural change** (radio, tab, row added or removed). **Text input updates the model without re-rendering** (so focus survives).
- **The form model and dynamic branching**: one `form` object is the truth. Branches (value fields per type, targets per range, issuance methods) go model → conditional render. Repeating blocks (N rules) are arrays.
- **Validation**: on save, `validate()` → red field + helper error + toast, scrolled to the first error. Rules (blocking mutually exclusive options, say) are disabled at render time and re-checked at save time.
- **Holding state**: save → push to the array → the list reflects it, with an auto id and a success toast. Implement the dialogs as drawn (leave-confirmation, delete, deactivate).
- **Custom widgets** (a multi-select, for example): one piece of state (open, selected), closing on an outside click. Mark with something like `data-stop` so the delegated handler can tell inside from outside.
- **Fidelity**: labels, terminology, order, and seed data exactly as Figma has them. **An obvious clone-residue mislabel** (a coupon list whose column reads 'product ID') is corrected to fit the context but **stated in the report** — no improving on your own; report it.
### 4. Verify in a browser (mandatory)
- **claude-in-chrome cannot open `file://`** → serve with `python3 -m http.server <port>` (from the output folder) and `navigate` to http://localhost:<port>/<file>.
- **Actually click** the core flow through claude-in-chrome: list render / dynamic form branching (one or two representatives) / validation (the error showing) / save → list reflection / detail / dialogs. Bundle clicks and screenshots with `browser_batch` to cut round trips (coordinates are relative to the previous screenshot).
- Fix bugs where they are found and **re-confirm** (never wave one through on a screenshot alone). Shut the server down when verification ends. The user opens the file with a double click (file://) — zero external resources, so it runs.
- A click right after a refresh can land before the render (timing) — do not pile navigate and clicks into one batch; confirm the load first.
### 5. Handoff report
- Where the deliverable is, and **which flows were verified** (what was actually pressed).
- Scope (In/Out): the flows included versus what was left out (cases not demonstrated, whether it was published).
- Mislabels corrected, mock-up limits (reset on refresh, and so on), and non-working elements (a search box that is only shown) — honestly.
## Publishing (optional)
An external write, so it goes through the **preview → "go"** gate. Check repo, account, and visibility in `tools.proto_publish` (for an internal screen, confirm visibility without fail). When the setting is `null`, ask where it should go first. Being a single HTML file, **one file** goes up with no screen folder. Being a code rebuild, there is no need for automatic pixel-diff refreshes — edit the source, re-verify, re-push.
## Constraints
- A code rebuild — the visuals are an approximation (not 100% pixel). If pixel fidelity comes first, this is the wrong approach. **Image assets are not part of the approximation — the originals get inlined** (step 3).
- **Never build behaviour the design does not have.** Turning a static element into something that "responds to a value" is not an improvement; it is inventing a spec that does not exist. Confine dynamic handling to elements whose state variants are drawn in the design, and where an interpretation went in, state it in the report.
- No project-specific proper nouns hardcoded in this document (repo, account, file key, particular screen names) — those come from memory.
- Labels, terminology, and seed data match Figma — do not improve them; report mislabels.
- Never declare "it works" without verifying — actually click it in a browser.
- `file://` will not work → verify through http.server.
## Notes
- The four sources complement each other: `get_design_context` = structure and coordinates, the TEXT walk = strings (labels, seeds, hidden), screenshots = visual tokens, the IMAGE fill scan = assets to fetch as they are. No single one of them does the job.
- REST `get_screenshot` can return an old state right after a write (stale) → work around it with an inline `node.screenshot()` render (rare here, this skill being mostly reads).
- Project-specific values are read from the `tools` section of the config. Do not write repos, accounts, file keys, or screen names into this document.
- Related skills: `/fig:read` (structure only), `/fig:code` (into a frontend repo), `/fig:lint` (auditing what is drawn).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!