Measuring Figma Designs
Overview
A Figma file is source data containing defects, not a specification. Screenshots show you what a frame looks like; they cannot show you hidden nodes, tofu codepoints, stacked duplicates, impossible padding, or a blur that is actually a copy of a photograph. Those only appear in the CSS.
Core principle: measure every value, infer none, and record every place you deliberately don't match the file.
The output is two documents: a CSS spec (the authority for how it looks) and a deviations register (every place the file is defective and what ships instead). Without the second document, "pixel-perfect" silently means "faithfully reproduced the bugs".
When to Use
- Implementing a Figma screen where fidelity matters
- Auditing whether shipped code matches the design
- Migrating a flow, a role, or a whole app from a redesign
- Adding a second breakpoint from a different Figma file or page
- Any time you catch yourself about to write a number you did not measure
Not for: one screen you'll eyeball, or design work where the Figma file is a sketch rather than a spec.
The Iron Rules
Each of these exists because violating it produced a wrong answer that survived until something else contradicted it.
Screenshots establish composition. Never values. Use them to learn what a screen is and what it does. Never to read a size, colour, spacing, radius, or a background mechanism.
Every value comes from
get_design_context.get_metadatais allowed for two things only: positions and box sizes of nodes that are neither rotated nor flipped, andhiddenflags. A text node's height is its layout box, not its font size.⚠️ Flipped is not the same as rotated, and it is easy to miss. For a node inside a
-scale-y-100or-scale-x-100wrapper,get_metadatareports the pre-flip origin — which can place an element outside its own parent. Measured instances: a fade read aty 603when it actually sits at496–603, and progress segments read aty 4inside a4px-tall frame. A position that is impossible (below its container, outside the frame, at the far edge of a box it should fill) is the tell. Take flipped and rotated nodes fromget_design_context.Never infer a state from its sibling. "It obviously only differs in the footer" was wrong by four values and a whole anchoring change. Pull every state.
Never derive one breakpoint's value by scaling another's. Measured ratios on a single frame spanned 1.00–1.73. Some components are byte-identical across breakpoints; others aren't. Measure; never compute.
Establish rules from at least two frames per family before generalising. Rules derived from one frame will be wrong for the rest — canvas colour, chrome, shell structure and header construction all varied by family in a file that looked uniform.
Absence from the spec means "not yet measured", never "nothing to specify". State coverage as two numbers: frames viewed and frames measured.
Record retractions in place. When a measurement overturns something you wrote, strike it, say what was wrong, and say why it was wrong — usually "because it came from a screenshot".
1:1 by default; every departure justified in the register. Reproducing a defect faithfully is not fidelity. Deciding silently not to is not either.
Workflow
digraph figma_measure {
"Check access + statefulness" [shape=box];
"Inventory: frames + composition" [shape=box];
"Measure 2+ frames YOURSELF" [shape=box];
"Write rules (§0)" [shape=box];
"More than ~8 frames left?" [shape=diamond];
"Fan out: 1 part file per agent" [shape=box];
"Measure serially into spec" [shape=box];
"Editorial merge + reconcile" [shape=box];
"Decisions gate" [shape=doublecircle];
"Check access + statefulness" -> "Inventory: frames + composition";
"Inventory: frames + composition" -> "Measure 2+ frames YOURSELF";
"Measure 2+ frames YOURSELF" -> "Write rules (§0)";
"Write rules (§0)" -> "More than ~8 frames left?";
"More than ~8 frames left?" -> "Fan out: 1 part file per agent" [label="yes"];
"More than ~8 frames left?" -> "Measure serially into spec" [label="no"];
"Fan out: 1 part file per agent" -> "Editorial merge + reconcile";
"Measure serially into spec" -> "Editorial merge + reconcile";
"Editorial merge + reconcile" -> "Decisions gate";
}
Phase 0 — Access and statefulness
The desktop Figma MCP reads whichever document is the active tab. Nodes from any other file return "No node could be found". Confirm reachability with one cheap call before planning anything, and tell the user not to switch tabs while agents are running.
A remote/API connector takes a file key explicitly and ignores tabs, but may require editor access. If it 403s, the desktop app is your only route — say so early rather than discovering it mid-fan-out.
Phase 1 — Inventory
get_metadata on the section to list frames (id, name, size). Then screenshot each frame to learn
composition only. Record a frame-by-frame audit table: node id, what the screen actually is, evidence
level. Expect frame names to lie — duplicate numbers, missing numbers, two frames sharing a name.
Address frames by node id forever after.
Phase 2 — Measure two or more frames yourself, then write the rules
Do not delegate this. Pull get_design_context on the most structurally distinct frames and derive a
§0 rules section covering:
- Device/browser chrome to subtract — mock status bars and browser bars are not app UI, and the offset they imply applies to every subsequent value
- Canvas fill, read from the frame — not from a bound variable, which may report something else
- Whether a token layer exists. Run
get_variable_defs. Mostly-literals is normal; a handful of real tokens among literals is a signal worth chasing - Shell structure — rails, headers, gutters, content column; and whether "screens" are pages, modals, or dialogs. Verify per family (Rule 5)
- Type scale, with actual ratios if a second breakpoint exists
- Which rules from a previous breakpoint's spec carry over and which explicitly do not
Phase 3 — Fan out
One agent per 2–4 frames. Each brief must contain:
- The rules file to read first, and the house table style to match
- Its exact node ids, with a "stop and report if unreachable, do not guess" instruction
- The specific measured values from comparable frames to compare against — not "match mobile"
- The defect taxonomy to hunt (see
defect-taxonomy.md) - The product decisions that kill UI, so nobody specifies dead elements
- One output file per agent — never a shared file, or edits collide
Deliberately give one shared-component claim to two agents who cannot see each other's work. This is the single highest-value trick in the workflow: independent agreement is evidence, and independent disagreement finds real errors that a single pass asserts away.
Use agent-brief-template.md as the starting point.
Phase 4 — Editorial merge
A separate agent with fresh context, no Figma calls. Renumber sections contiguously, merge all deviation candidates into one register with duplicates collapsed into single rows citing every affected frame, and produce:
- A reconciliation section — every cross-agent disagreement, both readings, and a verdict or an explicit "needs a human call". Hunt for unflagged ones too.
- A cross-breakpoint section if applicable — what is proven shared at ratio 1.0 and what genuinely diverges, each with its evidence.
- A blockers section — everything needing a human decision.
- An asset inventory — url → element → frames, flagging hashes shared across files.
Preserve every ⚠️ trap call-out and every retraction verbatim. Losing them re-opens settled questions.
Phase 5 — Decisions gate
Do not write an implementation plan yet. Surface the decisions that no amount of measuring resolves: contradictions between frames, values that fail accessibility, controls with no states, copy written for the wrong role, screens the flow requires that nobody designed. Get rulings, then plan.
Quick Reference
| Need | Tool | Notes |
|---|---|---|
| Any size/colour/type/radius/spacing | get_design_context |
the only source of values |
| Position or box of an unrotated node | get_metadata |
rotated nodes report a different envelope |
| Hidden nodes | get_metadata hidden flags |
invisible to get_design_context |
| Token layer | get_variable_defs |
may return {}; may disagree with the frame |
| What a screen is | get_screenshot |
composition only, never values |
| Assets | urls in get_design_context |
curl them; never re-trace an icon that exists |
Common Mistakes
| Mistake | Consequence |
|---|---|
| Reading a background off a screenshot | A blurred copy of a photo becomes "a second canvas colour" — mechanism inverted |
| Generalising rules from one frame | Canvas, rail and header rules wrong for two of three shells |
| Inferring a sibling state | Four wrong values and a missed anchoring change |
| Trusting a bound variable over the frame fill | Every screen 12 units too dark |
| Transcribing padding on a fixed-size box | 105px of content specified inside a 79px box |
| Transcribing every radius | Four radii for one control that all render identically |
| Taking a text node's height as its font size | 39px box read as a 39px font when the font is 32px |
| Counting nodes to count UI | Ten rectangles for nine tiles; ~90 nodes rendering zero pixels |
| Normalising an odd value early | A deliberate second blue dismissed as a leftover |
| Measuring everything before ruling on scope | Sub-pixel work on rows a product decision had already deleted |
Red Flags — stop
- About to write a number you did not measure
- About to say "it's the same as X" without pulling X
- About to scale a value between breakpoints
- About to write a rule from a single frame
- About to "clean up" an odd value without recording it
- A frame's header is not where the other frames' headers are — find the wrapper, don't correct the number
- A value looks arbitrary — look for a hidden sibling before transcribing it
Files
defect-taxonomy.md— the ~20 defect classes, how to detect each, what to ship insteadagent-brief-template.md— fan-out brief template