# Visual Acceptance

> Render a page in a real browser, compare it against a reference design image (PNG/JPG/WebP or a Figma export), then diagnose, fix and verify the difference with pixel, layout, DOM and runtime evidence. Use when asked to make a page match a screenshot, implement a design pixel-perfectly, fix visual or responsive differences from a mockup, run visual QA or a visual regression check against localhost, or check that frontend code matches Figma. Not for backend-only work, API behaviour, or accessibility audits on their own.

- Skill: `lin-0427/visual-acceptance` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add lin-0427/visual-acceptance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lin-0427/visual-acceptance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: LIN-0427 (https://skillmd.com/u/lin-0427)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/lin-0427/visual-acceptance

---


# Visual Acceptance

A coding agent can write the UI. This skill makes it prove the UI is right.

Reference design → real browser → visual inspection → diagnosis → code fix → re-render → objective verification.

The whole point is to replace "this should be close enough now" with a measurement: a browser screenshot, a pixel comparison, layout and runtime evidence, and an explicit status of PASS, FAIL, PARTIAL or BLOCKED.

## The one rule

Never report a visual result you did not measure. If the numbers do not pass, the status is not PASS, no matter how the page looks to you. Conversely, do not chase a PASS by weakening the test (see [Hard rules](#hard-rules)).

## Setup

The scripts in this skill need Node 20+ and Playwright with Chromium:

```bash
cd <skill-dir>            # the folder that contains this SKILL.md
npm install               # installs playwright, pixelmatch, pngjs
npx playwright install chromium
```

If the machine cannot download Chromium, the tool falls back to an installed Chrome or Edge channel; it reports BLOCKED only when no browser can start.

## Inputs

Ask for the two things the loop needs if they are not already in the request, then proceed:

1. **The reference design** - a PNG/JPG/WebP export, or a Figma frame the user exported. A Figma URL alone is only usable if a Figma connector or `FIGMA_ACCESS_TOKEN` is available; otherwise ask for a PNG export of the frame.
2. **The target** - a URL (`http://localhost:3000`, or any deployed URL) and, when it is not the current directory, the project root so source files can be located.

Everything else has defaults: the viewport is inferred from the reference image, dev servers are started or reused automatically, artifacts go to `<project>/.visual-acceptance/`.

## Quick start

```bash
# 1. Make sure the page is reachable (start the dev server, or let the audit do it).
node <skill-dir>/scripts/visual-audit.mjs \
  --reference ./design/home.png \
  --url http://localhost:3000 \
  --project .

# 2. Read the report, fix the biggest difference, run the same command again.
cat .visual-acceptance/runs/<run-id>/report.md
```

Exit codes are part of the contract: `0` PASS, `1` FAIL or PARTIAL, `2` BLOCKED.

Supported inputs:

- reference: PNG, JPG/JPEG, WebP, GIF, AVIF (non-PNG is decoded by the browser)
- target: any URL, plus `--route /pricing`
- viewport: `--viewport 390x844` or a preset (`mobile-small`, `mobile`, `tablet`, `desktop`, `desktop-large`); repeatable
- several references: `--reference desktop.png --reference mobile.png` (paired with viewports), or `design.png@390x844` to bind one reference to a viewport
- `--full-page` for long designs, `--selector .hero` to compare a single element
- `.visual-acceptance.json`, `visual-acceptance.config.json` or `.visual-acceptance/config.json` for anything you would otherwise repeat

## The loop

Run this loop deliberately. Each step produces evidence that the next step consumes.

1. **Render the baseline.** Run the audit unchanged. This gives the first real numbers: pixel mismatch, diff regions, layout issues, runtime errors, failed assets.
2. **Inspect the artifacts, not just the numbers.** Open `diff.png`, `overlay.png` and `regions.png`. The overlay (50% reference, 50% actual) is the fastest way to see what moved. Read `report.md` for the region list and the largest failures.
3. **Read the DOM evidence** (`dom.json`) for the elements inside the largest diff regions: bounding boxes, font, size, line height, colour, radius, padding, margin, overflow, `object-fit`. This is what turns "the hero looks off" into "h1 is 32px but should be 40px".
4. **Read the runtime evidence** (`console.json`, `network.json`, `layout.json`) before touching CSS. A 404 image, a failed webfont or an uncaught exception explains many "visual" problems and must be fixed first.
5. **Locate the source.** Search the project for the class name, test id, copy text or component name from the DOM evidence. Fix the component or its stylesheet, not global CSS.
6. **Write down the hypothesis** you are acting on, one sentence: "Header is 12px taller than the design because the logo row uses padding instead of a fixed height."
7. **Fix in priority order** (see below) and change as few things as possible, so the next run attributes cause to effect.
8. **Re-render and re-measure** with the same command. The session records the iteration, the files that changed and the verdict against the best result so far.
9. **React to the verdict.** `IMPROVED` → continue. `UNCHANGED` → the hypothesis was wrong; stop and re-diagnose instead of tweaking the same value again. `REGRESSED` → roll back that iteration (`node <skill-dir>/scripts/rollback.mjs --to-best`) and try a different hypothesis.
10. **Stop at the acceptance rules, not at a feeling.** When every enforced check passes, report PASS with the numbers. When the iteration budget or the improvement floor is reached without passing, report PARTIAL with the remaining issues.

Two consecutive iterations that move the metric by less than `minImprovement` (default 0.5% of pixels) mean the loop is stuck: stop and report PARTIAL rather than grinding.

## Fix priority order

Always fix in this order; skipping ahead is how agents spend five iterations on a shadow while the layout is wrong.

1. missing or broken assets (images, fonts, icons)
2. page structure and container widths
3. major positioning and grid/flex layout
4. spacing (padding, margin, gap)
5. typography (family, size, weight, line height, letter spacing, wrapping)
6. image geometry (`object-fit`, `object-position`, aspect ratio)
7. colours and backgrounds
8. borders, radii, shadows
9. sub-pixel details

## Reading the report

`report.json` and `report.md` contain the same data. The per-target metrics are:

| Metric | Meaning |
| --- | --- |
| `pixelMismatch` | Share of compared pixels that differ beyond `pixelThreshold`, excluding anti-aliased edges and masked regions |
| `meanPixelDelta` | Average colour distance over the whole page; catches a large area that drifts slightly below the per-pixel threshold |
| `horizontalOverflow`, `elementOverflow` | Content wider than the viewport |
| `clippedText`, `zeroSize`, `overlappingInteractive`, `unexpectedHidden` | Layout defects that change what the user can see or click |
| `consoleErrors`, `pageErrors` | Uncaught exceptions and `console.error` output |
| `failedAssets`, `failedFonts`, `failedResources` | Broken images, failed webfonts, failed or 4xx/5xx requests (`/favicon.ico` is ignored by default) |
| `stability` | 1 when two captures of the same page differed: the pixel numbers cannot be trusted until the dynamic element is frozen or ignored |
| `maskCoverage` | Share of the page excluded from the comparison; above 35% a PASS is meaningless and above 60% the run fails |

Pixel mismatch measures rendering, not meaning: a page can render the wrong content perfectly or the right layout with a substituted font. Treat the metrics, the diff regions, the DOM evidence and the runtime log as one signal set, and say which one supports each conclusion.

A reference exported at 2x (Figma's default) is detected and compared at its own
scale: the audit renders at the same device pixel ratio, so the comparison stays
1:1 and the numbers mean what they say. If you force `browser.deviceScaleFactor`,
the reference is resampled instead, and the report warns that resampling alone
costs 1-2% of pixels on text.

Thresholds are configured, never invented at runtime:

```json
{
  "acceptance": {
    "pixelMismatch": 0.03,
    "meanPixelDelta": 0.02,
    "horizontalOverflow": 0,
    "consoleErrors": 0,
    "failedAssets": 0,
    "stability": 0
  },
  "criticalRegions": [
    { "name": "hero-cta", "selector": ".cta", "pixelMismatch": 0.01 }
  ]
}
```

Critical regions matter for small, important elements: a wrong button radius or icon is a fraction of a percent of the page and will pass a page-wide threshold, so pin the region you actually care about.

## Dynamic content

Unstable pages cannot be measured. Before blaming the implementation:

- the tool already freezes `Date.now()` and `Math.random()`, disables CSS animations/transitions and carets, pauses videos, waits for fonts, images and network to settle, and verifies that two captures match
- if a region still changes, ignore the **container** that holds it, not the changing text node: `--ignore '[data-testid="clock"]'` or `--mask x,y,w,h`
- a run that reports `stability: 1` is not a PASS, whatever the pixel number says

## Multi-viewport and responsive work

- several viewports in one run: `--viewport 1440x900 --viewport 390x844` with matching references
- a desktop fix that breaks mobile is a REGRESSION and is reported as one; always re-check every active viewport after changing layout code
- check `horizontalOverflow` first on narrow viewports: overflow is a hard failure, not a pixel difference
- the reference image decides the default viewport: a 390x844 reference is rendered at 390x844, not at desktop size

## Best-score protection and rollback

Every run appends an iteration to `.visual-acceptance/session.json` with its metrics, status, changed files and a git diff. The audit refuses to treat a worse result as progress and prints the rollback command.

```bash
node <skill-dir>/scripts/rollback.mjs --list          # iterations, metrics, changed files
node <skill-dir>/scripts/rollback.mjs --to-best       # undo the iterations after the best one
node <skill-dir>/scripts/rollback.mjs --iteration 2 --dry-run
```

Rollback restores only the files those iterations changed, from snapshots taken before each iteration. It never runs `git reset`, `git clean` or `git checkout`, never touches the index, and skips any file whose current content is not what the iteration produced unless `--force` is given. Note that this also protects edits made *during* the session: they are restored to the session baseline, so run it deliberately.

## Statuses

Report exactly one of these, with the evidence attached:

- **PASS** - every enforced check passed for every target.
- **FAIL** - the page rendered and was measured, and at least one enforced check failed.
- **PARTIAL** - the loop stopped (iteration budget or no further improvement) before passing; list the remaining differences.
- **BLOCKED** - the audit could not produce trustworthy evidence: no reachable page, no browser, an unreadable or dimension-less reference, an authentication wall, a required backend that is down. State the blocking condition; never convert it into a PASS or a FAIL.

When nothing passes, report the remaining mismatch, the threshold it missed and the regions involved. Do not describe a failure as "very close".

## Hard rules

These are not negotiable, and they exist because each one has been used to fake a result:

- never modify, regenerate or replace the reference image to make a run pass
- never widen `pixelMismatch`, `pixelThreshold` or an acceptance rule during a session to turn a FAIL into a PASS
- never mask a large region, hide elements, or delete content to raise the score; masking above 35% is reported, above 60% fails the run
- never disable the stability check to accept an animated page
- never `git reset --hard`, `git clean -fd`, `git checkout .`, `git stash`, force push, publish or deploy
- never overwrite pre-existing uncommitted user changes; the audit records them at session start and rollback is per-file and hash-guarded
- never edit files outside the project root, and never touch credentials, `~/.codex`, or system configuration
- only stop processes the audit started; reuse a running dev server instead of killing anything on a port

Auth state lives in `.visual-acceptance/auth.json` (Playwright `storageState`), is added to `.gitignore` automatically, and must never be logged, printed or committed. Console, network and report artifacts are redacted (cookies, authorization headers, tokens, passwords) before they are written.

## Git safety

Before the first iteration, note `git status` output. Pre-existing changes are reported in `report.json` (`session.preExistingChanges`) and must survive the whole session. When your fix is complete, report the changed files and let the user commit.

## References

Read only what the current problem needs:

- [references/workflow.md](references/workflow.md) - the full loop in detail, with the exact commands and report fields per step
- [references/visual-debugging.md](references/visual-debugging.md) - how to read diff regions, overlays and mismatch numbers, and how to turn them into a hypothesis
- [references/responsive.md](references/responsive.md) - breakpoint work, overflow diagnosis, multi-viewport runs
- [references/typography.md](references/typography.md) - font loading, fallbacks, line-height, letter-spacing, wrapping differences
- [references/image-debugging.md](references/image-debugging.md) - crop, `object-fit`, aspect ratio, broken and blocking assets
- [references/browser-runtime.md](references/browser-runtime.md) - console, network, storage state, dev-server and port handling
- [references/safety.md](references/safety.md) - rollback semantics, artifact layout, redaction, and what must never be automated

## Scripts

| Script | Purpose |
| --- | --- |
| `scripts/visual-audit.mjs` | Full loop: render, compare, diagnose, report, record iteration |
| `scripts/capture.mjs` | Screenshot + evidence only, no reference needed |
| `scripts/compare.mjs` | Compare two image files (no browser needed for PNG pairs) |
| `scripts/inspect-dom.mjs` | DOM, boxes and computed styles |
| `scripts/inspect-runtime.mjs` | Console, page errors, failed requests and assets |
| `scripts/stabilize.mjs` | Is the page deterministic between captures? |
| `scripts/report.mjs` | Re-render or summarise an existing run |
| `scripts/rollback.mjs` | Undo the file changes of recorded iterations |

Each script accepts `--help`. Config schema: [schema/visual-acceptance.schema.json](schema/visual-acceptance.schema.json).

Ready-made starting points for the five common jobs (spacing, typography, image
crop, responsive overflow, multi-viewport) are in [examples/](examples/README.md).

