SuperAPI AI Directory

design-diff

Design & creation

View repository

Updated 10-16 08:06

About this skill

Design Diff, a visual feedback loop

Overlay a design on a screenshot of a live page at the same pixel size and get back two numbers you act on.

  • matchPercent, how close the page is to the design. Read it to know when to stop.
  • diffBounds, the box in CSS pixels around what is still wrong. Aim the next edit here.

Run it, read the score, fix what the box points at, run again, and stop when the score clears your bar. The rest of this skill makes those two numbers trustworthy.

When to use

  • Building a page or component from a design export or a Figma frame.
  • Closing a design to code handoff, or hunting down visual drift.
  • Verifying a built page matches a mockup before opening a PR.
  • Gating CI on a minimum visual match.

Prerequisites

  • Bun. Check with bun --version. The CLI runs via bunx design-diff. The first run downloads Chromium once, then every run is instant.
  • A running page URL like http://localhost:3000, or a local screenshot PNG.
  • A design source. A local PNG, or a Figma fileKey and frameId with DESIGN_DIFF_FIGMA_TOKEN set. Read-only access is enough.

The loop

  1. Get a baseline. Run the tool against the page and the design. Prefer --json so you can parse the result directly.
    bunx design-diff http://localhost:3000 --png design.png --scale 1 --json
    
  2. Read the two values from the JSON. matchPercent is the headline score. diffBounds is { x, y, width, height } in CSS pixels, or null when there is no diff.
  3. Diagnose from the shape, not just the number. See the table below.
  4. Make one targeted edit aimed at diffBounds, then run step 1 again.
  5. Stop when matchPercent clears your bar. Real pages settle 1 to 2 points below 100 from anti-aliasing and font rendering, so pick a threshold instead of chasing a perfect score.

Reading the diff shape

The shape is the diagnosis. Read it before you open any report.

Signal Diagnosis
Tight box, low coveragePercent One component is off. Wrong color, size, position, or spacing.
Box spanning the page, high coveragePercent A global problem. A font that never loaded, a viewport or size mismatch, or a layout shift.
readiness.fontsReady is false Captured before web fonts loaded. The score is not trustworthy yet. Wait for readiness, see below, and run again.
diffBounds is null No differences at this threshold.

Key flags

--png <path>            Design export (PNG). Viewport is derived from it.
--file <key> --frame <id>  Pull the export straight from Figma instead of --png.
--actual <path>         Compare a local screenshot instead of visiting a url. No browser.
--scale <n>             Match the export. 1 for 1x, 2 for retina. Default 1.
--threshold <0..1>      Per-pixel color sensitivity. Default 0.1. Lower is stricter.
--ignore <x,y,w,h>      Mask a fixed rectangle in CSS px. Repeatable.
--ignore-selector <sel> Mask every element matching a CSS selector. Repeatable.
--wait-for <sel>        Hold capture until a selector appears. 15s timeout. Repeatable.
--auth <path>           Playwright storageState JSON for pages behind login.
--fail-under <pct>      Exit 1 when matchPercent is below this. CI gate.
--json                  Print only the metrics object to stdout.
--annotate              Also write annotated.png with the diff box drawn on the page.
--no-overlay            Skip the HTML report and heatmap. Faster, metrics only.
--out <dir>             Output dir. Default .design-diff.
--open                  Open the HTML report when done.

Full walkthrough in How it works. See every flag with bunx design-diff --help.

Common scenarios

Tight agent loop, you already have a screenshot. Skip the browser and diff two PNGs. Fast, offline, sandbox friendly. Bounds come back in image pixels and --scale is ignored.

bunx design-diff --actual screenshot.png --png design.png --json

Pull the design from Figma. The frame id is the one in the frame's URL, like 10-2.

export DESIGN_DIFF_FIGMA_TOKEN=figd_your_token
bunx design-diff http://localhost:3000 --file abc123 --frame 10-2 --json

Mask what is meant to change, like avatars, timestamps, and live counters, so it never counts as a diff. Prefer --ignore-selector when the region moves or resizes.

bunx design-diff http://localhost:3000 --png design.png \
  --ignore-selector "[data-dynamic], time, .avatar"

Wait for readiness when the page fetches content late. It fails loudly if the selector never appears within 15s.

bunx design-diff http://localhost:3000 --png design.png --wait-for ".hero-loaded"

Pages behind login. Save a session once, then reuse it. No credentials touch the tool.

bunx playwright codegen --save-storage=auth.json https://your-app/login
# log in in the window, then close it
bunx design-diff https://your-app/dashboard --png design.png --auth auth.json

CI gate. Turn the visual match into pass or fail.

bunx design-diff http://localhost:3000 --png design.png --json --fail-under 98

Retina export. Pass --scale 2 so a 2x PNG is measured honestly.

Programmatic API, for looping in code

The CLI is a thin wrapper over designDiff, which returns the same data written to metrics.json, including artifact paths.

import { designDiff } from "design-diff";

const result = await designDiff({
  url: "http://localhost:3000",      // or actual: "screenshot.png" for image-vs-image
  design: "design.png",              // or { fileKey, frameId }
  scale: 1,
  threshold: 0.1,
  ignore: [{ selector: "[data-dynamic]" }, { x: 24, y: 24, width: 48, height: 48 }],
  waitFor: ".hero-loaded",
});

if (result.matchPercent < 98) {
  // aim the next edit at result.diffBounds, then run again
}

Looping over many pages? Reuse one browser instead of launching Chromium each time.

import { designDiff, launchBrowser } from "design-diff";

const browser = await launchBrowser();
try {
  for (const url of urls) {
    await designDiff({ url, design: "design.png", browser });
  }
} finally {
  await browser.close();
}

Gotchas

  • --fail-under and --threshold are different. --fail-under is a percentage gate on the whole-page matchPercent. --threshold is per-pixel color sensitivity passed to pixelmatch, where lower is stricter. Do not conflate them.
  • Same size or hard error. The design and the page must share dimensions. A large mismatch fails by design, so match --scale to the export.
  • A high score is necessary, not sufficient. A shifted component can hide inside a high match. Keep the diff box as the final check, plus the overlay or annotated image for a human.
  • --actual mode has no page. So --wait-for, --ignore-selector, and readiness do not apply, and bounds are in image pixels.
  • Trust the score only when the page was caught ready. When readiness.fontsReady is false, use --wait-for and run again before you act on the number.