Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .agents/skills/design-diff/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
name: design-diff
description: 'Measure how closely a live web page matches a design, pixel for pixel, and iterate until it matches. Use this whenever the user builds or checks a UI against a design, a PNG or a Figma frame, closes a design handoff, chases visual drift, verifies a page looks like a mockup, or gates a build on visual match, even when they never say design-diff by name. Drives the design-diff CLI and library. Read matchPercent and diffBounds, aim the next edit at the box, repeat.'
argument-hint: '<url> and a design source, --png <path> or Figma --file and --frame'
---

# 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](https://bun.sh). 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.
```sh
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](../../../docs/HOW_IT_WORKS.md). 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.
```sh
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`.
```sh
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.
```sh
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.
```sh
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.
```sh
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.
```sh
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.

```ts
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.
```ts
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.
Loading