Figma Overlay Fidelity Check
Core idea: export the Figma design as a PNG, then use Playwright's browser_evaluate to inject it at runtime as a top-level overlay on the page, combined with pixel diffing for quantified results. Because the injection happens at runtime, you must not (and need not) modify project source code — a page refresh clears everything.
Workflow
Task Progress:
- [ ] Step 1: Export the design (mind the 4096px limit)
- [ ] Step 2: Align viewport + height sanity check
- [ ] Step 3: Inject overlay (opacity for coarse pass → difference for fine pass)
- [ ] Step 4: Region cropping + DOM measurement to locate and fix diffs
- [ ] Step 5: Quantify with pixel diff
- [ ] Step 6: Cleanup
Step 1: Export the design
Use Figma MCP download_assets (defaultFormat: png) to export the target Frame, save it to /tmp/figma-overlay/design.png, and record the Frame's logical width/height (e.g. 1400×4283).
Key gotcha: Figma caps exports at 4096px on the long edge. Long pages get proportionally downscaled on export (e.g. 1400 wide becomes 1339). Use file or sips to confirm the actual pixel size. The overlay is unaffected (CSS stretches it back to logical width), and the Step 5 diff script auto-resamples widths, so no manual scaling is needed.
Step 2: Align viewport + height sanity check
Use browser_resize to set the viewport width to the design's logical width (viewport mismatch is the most common source of false positives).
One-line sanity check: document.body.scrollHeight should be within a few px of the design's logical height. A large gap means module-level spacing/height bugs — fix the structure first before pixel-level comparison.
Step 3: Inject the overlay
Ways to make the image accessible to the page, in order of preference:
- Dev server static directory (recommended): copy design.png to something like
public/__figma_overlay.png, injectsrc="/__figma_overlay.png", delete the file when done - base64 data URL: fallback when there's no static directory; note very large images may exceed the evaluate argument size limit
Inject with browser_evaluate (waiting for web fonts and disabling animations along the way — fonts still on fallback produce large false text diffs):
async () => {
await document.fonts.ready;
document.getElementById('__figma_overlay__')?.remove();
const style = document.createElement('style');
style.textContent = '* { animation: none !important; transition: none !important; }';
document.head.appendChild(style);
const img = document.createElement('img');
img.id = '__figma_overlay__';
img.src = '/__figma_overlay.png';
img.style.cssText =
'position:absolute;top:0;left:0;width:1400px;' + // ← design's logical width
'z-index:2147483647;pointer-events:none;opacity:0.5;';
document.body.appendChild(img);
}
Two modes:
opacity:0.5: coarse pass for overall misalignmentopacity:1; mix-blend-mode:difference: fine pass. The blacker, the closer the match; bright outlines = position/size offsets; bright solid blocks = color mismatch; "ghosted" text = line-height/font-size mismatch
If the page has dynamic content (carousels, videos, live dates, random data), freeze it in the same evaluate call (pause videos, pin the carousel to the frame shown in the design, stub dates) — otherwise it shows up as constant noise in every pass.
Step 4: Locate and fix diffs
Full-page screenshots are too large to inspect details, so don't eyeball them — combine these techniques:
Machine-located regions (preferred): run the Step 5 pixel diff early — it prints the top mismatch regions as x/y/w/h bounding boxes sorted by severity. Crop those exact coordinates instead of scanning the image by eye; this also catches small-but-severe diffs that a <2% total score would hide.
Region cropping: use this skill's scripts/crop.mjs to crop suspicious regions out of the difference screenshot for a zoomed-in look:
node <path-to-this-skill>/scripts/crop.mjs diff.png <x> <y> <w> <h> out.png
DOM measurement: use browser_evaluate + getBoundingClientRect to measure absolute coordinates of key elements and compare the numbers directly against node coordinates from Figma metadata — much faster at pinpointing root causes than eyeballing the overlay:
() => [...document.querySelectorAll('h2,h3')].map((e) => {
const r = e.getBoundingClientRect();
return { text: e.textContent.slice(0, 20), top: r.top + scrollY, left: r.left + scrollX, w: r.width, h: r.height };
})
After each fix, refresh the page, re-inject the overlay, and re-check. Loop until the difference screenshot is essentially all black.
Note: Playwright MCP's browser_take_screenshot with a filename writes to the user home directory, not the project directory.
Step 5: Quantify with pixel diff
Refresh to remove the overlay, take a full-page screenshot, then compare:
cd /tmp/figma-overlay && npm init -y && npm i pixelmatch pngjs # first time only
node <path-to-this-skill>/scripts/pixel-diff.mjs design.png page.png diff.png
The script auto-resamples the wider image to the narrower width, so Figma's export downscale and retina screenshots (2x device pixel ratio on macOS makes screenshots twice the viewport size) are both handled — no manual sips step. It reports the overall mismatch percentage plus the top mismatch regions with x/y/w/h coordinates for direct cropping.
Pass criteria: mismatch < 2%, and every reported region is explainable as text anti-aliasing or photo-edge noise (low density, no large solid blocks).
Step 6: Cleanup
- Delete the temporary overlay image from
public/, refresh and confirm the overlay is gone - Confirm no overlay-related code exists in source (this skill never writes source code — if any exists, that's a violation)
- Clean up
/tmp/figma-overlay/and screenshot files in the home directory
Frequent Real-World Diff Causes (check these first)
| Symptom | Root cause | Fix |
|---|---|---|
| Ghosted text drifting down line by line | Figma normal line-height ≈ 1.2, Tailwind/browser default is 1.5 | Explicit leading-[Npx], N = the Figma text node's height |
| Photo content misaligned, bright blocks | Code uses object-cover center-crop while Figma has the image at exact left/top/width/height inside its container | Absolutely position per the image geometry from get_design_context + max-w-none |
| Buttons/cards a few px too tall | CSS border takes up box size, Figma strokes don't | Reduce padding accordingly when adding borders |
| A whole section shifted by tens of px | Extra/missing padding or gap, or a 0-width decorative node in Figma (inset overflow) mistaken for a spacer | Use DOM measurement to check numbers against Figma coordinates one by one |
| Image assets themselves don't match | Local asset is an old export or cropped from a different source | Re-export from Figma; if instance children can't be exported by id, export the parent instance at 2x and crop programmatically |
Rules
- Never write overlay code into project source (components, layouts, global styles — none of it); always inject at runtime
- Only compare a single page-level Frame, never the whole canvas
- For long pages, anchor the overlay with
position:absoluteto the top of the document so it scrolls with the page