Skip to content
Merged
Show file tree
Hide file tree
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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: CI

on:
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run typecheck
- run: bun test
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 planetabhi
Copyright (c) 2026 Abhimanyu Rana @planetabhi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# design-diff
# Design Diff

A CLI and library that overlays a design export on a screenshot of your page. Move a slider to compare them side by side, lower the opacity to see one on top of the other, or view a pixel diff to see exactly which pixels differ.
A coding agent can build a page and even look at it, but it cannot measure it against the design pixel for pixel. Design Diff does. It overlays the design on a screenshot of the live page at the same pixel size and returns one number for what matched plus the box around what did not. Call it, read the score, fix what the box points at, and repeat until the page matches.

**It reduces to two values an agent acts on.**

- **`matchPercent`** — how close the page is to the design.
- **`diffBounds`** — the box around what is still wrong.

Read the first to know when to stop, aim the next edit at the second, and loop. Everything else in this README exists to make those two numbers trustworthy.

Built agent-first. An agent or script drives it and reads back plain JSON. People get a slider and heatmap report to eyeball the same diff. And CI gets a hard pass/fail gate for merges.

## Usage

Expand All @@ -20,7 +29,7 @@ bunx design-diff http://localhost:3000 --file <fileKey> --frame <node-id> --scal
--frame <node-id> Figma frame to export (from the frame's URL).
--actual <path> Compare a local PNG instead of screenshotting the url.
--scale <n> Match the export: 1 for 1x, 2 for retina (default 1).
--threshold <0..1> Pixel-diff sensitivity (default 0.1).
--threshold <0..1> pixelmatch colour threshold, lower is stricter (default 0.1).
--auth <path> Playwright storageState for pages behind login.
--out <dir> Where the report is written (default .design-diff).
--ignore <x,y,w,h> Rectangle (CSS px) to exclude from the diff. Repeatable.
Expand All @@ -37,9 +46,9 @@ bunx design-diff http://localhost:3000 --file <fileKey> --frame <node-id> --scal

The design PNG sets the size, the page is screenshotted at the same pixel dimensions, so the two line up exactly. It waits for fonts and images to load and turns off animations before capturing.

With `--file/--frame`, it pulls the frame's size and PNG from the Figma API instead (needs `DESIGN_DIFF_FIGMA_TOKEN`).
With `--file/--frame`, it pulls the frame's size and PNG from the Figma API instead. Set `DESIGN_DIFF_FIGMA_TOKEN` to a Figma personal access token, created under Settings → Security → Personal access tokens. Read-only file access is enough, and view access to the file is all it needs. See [Figma's access token docs](https://www.figma.com/developers/api#access-tokens).

Use `--ignore` to mask dynamic regions (avatars, timestamps) so they never count as differences. Coordinates are CSS pixels, repeat the flag for multiple boxes. Prefer `--ignore-selector` when the region moves or resizes — every element matching the selector is masked by its live bounding box.
Use `--ignore` to mask dynamic regions (avatars, timestamps) so they never count as differences. Coordinates are CSS pixels, repeat the flag for multiple boxes. Prefer `--ignore-selector` when the region moves or resizes. Every element matching the selector is masked by its live bounding box.

```sh
bunx design-diff http://localhost:3000 --png design.png \
Expand All @@ -60,7 +69,7 @@ Skip the browser entirely and diff two local PNGs with `--actual`. Handy offline
bunx design-diff --actual screenshot.png --png design.png --json
```

No page means no `--wait-for`, `--ignore-selector`, or `readiness`. `--scale` is ignored too — bounds are reported in image pixels. Everything else works the same.
No page means no `--wait-for`, `--ignore-selector`, or `readiness`. `--scale` is ignored too. Bounds are reported in image pixels. Everything else works the same.

## Pages behind login

Expand Down Expand Up @@ -97,8 +106,9 @@ page coverage=100.0%
| **Diff bounds** | The box enclosing every changed pixel, in CSS pixels. A tight box means one component is off, a box spanning the page points at a layout, viewport, or font problem. |
| **Page coverage** | How much of the page that box spans. Tells a local issue from a global one. |

The shape of the diff is the diagnosis. A tight box is one component off, a box spanning the page points at a font that never loaded or a viewport mismatch. Read the shape, not just the number.

The same numbers are written to `metrics.json` in the run folder for scripting:
The same numbers are written to `metrics.json` in the run folder for scripting.

```json
{
Expand All @@ -115,7 +125,7 @@ The same numbers are written to `metrics.json` in the run folder for scripting:

The overlay report draws the diff bounds as a box you can toggle on and off. Pass `--no-overlay` to skip the HTML report and heatmap when you only need the metrics.

Pass `--annotate` to also write `annotated.png` — the page screenshot with the diff box drawn on it. It needs no browser to view, so it drops straight into a PR comment or CI artifact. Nothing is written when there is no diff.
Pass `--annotate` to also write `annotated.png`. The page screenshot with the diff box drawn on it. It needs no browser to view, so it drops straight into a PR comment or CI artifact. Nothing is written when there is no diff.

## Scripting and CI

Expand All @@ -127,7 +137,14 @@ bunx design-diff http://localhost:3000 --png design.png --json --fail-under 98
```

`--fail-under` compares against **Visual match** (a percentage). It is separate
from `--threshold`, which is the per-pixel colour sensitivity.
from `--threshold`, which sets how different a single pixel must be to count as
changed. `--threshold` is passed straight to
[pixelmatch](https://github.com/mapbox/pixelmatch): a pixel is flagged when its
YIQ-weighted colour distance from the design exceeds that fraction of the maximum
possible distance, so it is a perceptual metric, not raw Euclidean RGB. Lower is
stricter, `0` flags any difference at all, and `0.1` is the default.

For an agent the contract is simple. Read the JSON, act on `matchPercent` and `diffBounds`, run again, and stop when the score clears your bar.

## Programmatic API

Expand All @@ -142,7 +159,8 @@ const result = await designDiff({
design: "design.png", // or { fileKey, frameId }
scale: 1,
threshold: 0.1,
ignore: [{ x: 24, y: 24, width: 48, height: 48 }],
ignore: [{ selector: "[data-dynamic]" }, { x: 24, y: 24, width: 48, height: 48 }],
waitFor: ".hero-loaded",
});

if (result.matchPercent < 98) process.exit(1);
Expand All @@ -164,6 +182,13 @@ try {
}
```

## Limitations

- It compares pixels, not perception. Treat the score as a guide, not a verdict.
- Anti-aliasing and font rendering vary across machines, so a real page against a design export usually settles a point or two below 100 even when it looks right. Pick a threshold instead of chasing a perfect score.
- The design and the page must be the same size. A large mismatch is a hard error by design.
- A high score is necessary but not sufficient. A shifted component can still hide inside it, so keep the diff box and a human eye as the final check.

---

By [@planetabhi](https://planetabhi.com/) ⋛⋋( ⊙◊⊙)⋌⋚
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "design-diff",
"version": "0.0.4",
"description": "CLI and library for overlaying design exports or Figma frames on live pages, with a before/after slider and pixel-diff heatmap.",
"description": "Give agents and humans a precise, machine-readable way to measure how closely a live web page matches a design.",
"keywords": [
"design-diff",
"visual-regression",
Expand Down
Loading