Eyeballing "close enough" is how 4px drifts and off-token grays reach production. Screenshot the rendered implementation, screenshot the Figma frame, and have a multimodal model produce a structured discrepancy list — spacing, type scale, color, states — then fix, re-shoot, and repeat until the table is empty or intentional.
Tier: Core · Workflow area: Design-to-code & design systems · Tools: Claude / Gemini vision, Playwright screenshots, chrome-devtools-mcp
Human visual review fails predictably: we normalize what we've stared at, we miss systematic-but-small errors, and we never check all states. A vision model comparing two images side by side has no such attachment — it will flatly report that the card padding is 20px against a 24px design, in every variant, every time you ask.
toHaveScreenshot and keep the model for interpreting diffs.Capture both sides under controlled conditions. Same width, same data, same theme — or the diff is meaningless. Implementation side, via a Storybook story or live route:
npx playwright screenshot --viewport-size=1280,900 \
"http://localhost:6006/iframe.html?id=subscriptioncard--active&viewMode=story" \
shots/impl-active.png
Design side: export the frame from Figma at 1x (select frame → Export → PNG), or let the Dev Mode MCP fetch the screenshot if wired (pb-09). Name files by state: figma-active.png, impl-active.png.
Ask for a structured discrepancy table — never "do these match?" Open-ended comparison prompts produce reassurance; structured prompts produce findings:
Image 1 is the design (Figma export). Image 2 is my implementation.
Compare them systematically and output a markdown table:
| # | element | property | design value | implementation value | severity |
Check in this order:
1. Layout: alignment, gaps between siblings, padding at each edge
2. Typography: size, weight, line-height, color per text element
3. Color: backgrounds, borders, icons — name the closest token if you
can tell values apart
4. Shape: radii, borders, shadows
5. Content: truncation, icon choice, badge text casing
Severity: must-fix (visibly wrong) / minor (2-4px or shade-level) /
check-token (values differ but both look like system values).
Do NOT say "looks close" — if a row has no discrepancy, omit it.
Good output is 5–15 concrete rows ("Badge text: design 12px/600, impl 14px/500 — must-fix"), not prose. If you get prose, restate: "Table only, omit matching elements."
Convert findings to fixes — mapped to tokens, not magic numbers. Feed the table back to your coding agent:
Fix rows 1, 3, 4, 7 of the discrepancy table in SubscriptionCard.tsx.
Every fix must land on a design token: e.g. row 3's 20px padding
becomes p-6 (24px) per our scale — never a raw pixel value copied
from the design. If the design value has no matching token, flag it
for the designer instead of hardcoding.
Re-shoot and re-diff until the table converges. Loop steps 1–3. Convergence means every remaining row is classified intentional with a reason ("radius token differs from design; DS decision 2026-03"). Two to three iterations is typical. Then run the loop for every state: hover (capture via Playwright's page.hover in a small script or chrome-devtools-mcp driving the real page), error, empty, dark mode, and your narrowest supported width.
Attach the final table to the PR. The converged table — with intentional-deviation reasons — replaces "matches design ✅" in the PR description. Designers review deviations, not pixels; reviewers see evidence, not claims. For recurring surfaces, commit the capture script so the next change reruns the same loop in one command:
sh scripts/fidelity-shots.sh subscription-card && open shots/
Control the capture environment ruthlessly: identical viewport, deterministic data (MSW fixtures, frozen dates), fonts fully loaded (Playwright waits, but check custom-font FOUT), animations disabled (prefers-reduced-motion or a global CSS override in the story). Half of all "discrepancies" in sloppy setups are environment noise — and noise teaches the team to ignore the table.
xcrun simctl io booted screenshot shots/impl.png (iOS) or adb exec-out screencap -p > shots/impl.png (Android); diff against the mobile frame export; add a device-width pass since RN layouts drift differently per screen class.flutter test --update-goldens); use the multimodal loop above goldens to interpret failures and to compare against Figma, where goldens can't help.Replace "does this match?" with a converging discrepancy table: controlled screenshots in, structured findings out, token-mapped fixes, repeat. The vision model supplies tireless eyes; the discipline of the loop supplies the fidelity.