Imported from jnPiyush/AgentX (
.github/skills/design/visual-regression/SKILL.md). Install upstream withnpx skills add jnPiyush/AgentX --skill visual-regression. Copyright stays with the author.
Visual Regression
WHEN: A prototype is iterated more than once, or before declaring a prototype review-ready. This skill snapshots the key screens at the canonical breakpoints, diffs new builds against an approved baseline, and fails the audit if drift is unintended. Default engine is Playwright; managed services (Chromatic, Percy, Applitools) are listed for teams that prefer hosted review queues.
When to Use This Skill
- Iterating a prototype where each change can shift unrelated screens
- Hardening a working SPA prototype that will inform a production build
- Catching CSS regressions from token or theme changes
- Reviewing a PR that touches global styles or shared components
Visual regression complements axe (a11y) and Lighthouse (performance). It does not replace either.
Authoritative Sources
| Source | What it covers |
|---|---|
| Playwright Test, Visual comparisons | toHaveScreenshot, masking, threshold |
| Storybook + Chromatic docs | Component-level visual review |
| BrowserStack Percy docs | Hosted DOM snapshot + diff |
| Applitools Eyes docs | AI-driven layout-aware diff |
| Microsoft accessibility-insights and reflect.run | Adjacent tooling |
Default Engine: Playwright
Reasoning:
- Already required by
development/browser-automationfor the axe pass. - Free and runs locally and in CI.
- Deterministic with explicit threshold and animation handling.
Install (per prototype)
npm install --save-dev @playwright/test
npx playwright install --with-deps chromium
Project layout
docs/ux/prototypes/<feature>/
__screenshots__/
chromium-darwin/ # baseline images, committed to git
tests/
visual.spec.ts
playwright.config.ts
Baselines live next to the prototype, not in tests/ at the repo root, because they are tied to the prototype version.
Minimal config
playwright.config.ts:
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
use: {
baseURL: "http://localhost:4173",
screenshot: "only-on-failure",
trace: "retain-on-failure",
},
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.01,
animations: "disabled",
caret: "hide",
scale: "css",
},
},
projects: [
{ name: "mobile", use: { viewport: { width: 360, height: 800 } } },
{ name: "tablet", use: { viewport: { width: 768, height: 1024 } } },
{ name: "desktop", use: { viewport: { width: 1440, height: 900 } } },
],
});
Minimal test
tests/visual.spec.ts:
import { test, expect } from "@playwright/test";
const ROUTES = [
{ name: "home", path: "/" },
{ name: "dashboard", path: "/dashboard" },
{ name: "settings", path: "/settings" },
];
for (const route of ROUTES) {
test(`route: ${route.name}`, async ({ page }) => {
await page.goto(route.path);
await page.waitForLoadState("networkidle");
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot(`${route.name}.png`, {
mask: [
page.locator("[data-vr-mask]"),
page.locator("time"),
],
fullPage: true,
});
});
}
Run locally
npm run build && npx vite preview --port 4173 &
npx playwright test --update-snapshots # first run, accept baseline
npx playwright test # subsequent runs, fail on drift
Commit the generated baselines in __screenshots__/<browser-os>/. They are part of the prototype.
Determinism Rules
Visual regression is only useful if the only signal is intent. Eliminate noise mechanically:
- Disable animation at the test level via
animations: "disabled"and a CSS guard:
Add.vr-mode *, .vr-mode *::before, .vr-mode *::after { animation: none !important; transition: none !important; }vr-modeclass via PlaywrightaddInitScript. - Mask volatile content: timestamps, user avatars, live data. Use
data-vr-maskattributes. - Stabilize fonts: wait on
document.fonts.readyand prefer locally hosted WOFF2 over network fonts. - Fix the viewport per project: do not rely on default sizes.
- Fix the OS in CI: Playwright generates per-OS baselines. Pin to one OS (usually Linux in CI) and commit only those.
- Use
scale: "css"so HiDPI machines do not produce different baselines. - Seed any randomness in the prototype's data layer when the SPA renders sample data.
CI Integration
GitHub Actions example:
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npx vite preview --port 4173 &
- run: npx wait-on http://localhost:4173
- run: npx playwright test
- if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diff-report
path: |
playwright-report
test-results
On failure, the agent or developer downloads the artifact, inspects the diff PNGs (expected, actual, diff), and either fixes the source or accepts the new baseline with --update-snapshots. Baseline updates must be reviewed in the PR diff.
Updating Baselines
- Always update intentionally, with a commit message that names which screens shifted and why.
- One PR, one baseline update batch. Mixed code + baseline PRs lose review value.
- Reject baseline updates that include unrelated drift; fix the root cause first.
Acceptance Thresholds
| Mode | maxDiffPixelRatio |
When |
|---|---|---|
| Strict | 0 | Component / Storybook level |
| Default | 0.01 | Route-level prototype |
| Tolerant | 0.05 | Cross-OS smoke (avoid using as a primary gate) |
Stay at default. Increasing tolerance hides bugs.
Alternatives (hosted)
Use when the team prefers a managed review queue:
| Tool | Strength | Caveat |
|---|---|---|
| Chromatic | Tight Storybook integration, baseline review UI | Component-level, requires Storybook |
| Percy (BrowserStack) | DOM snapshot, multi-browser hosting | Per-snapshot pricing |
| Applitools Eyes | Layout-aware diff, ignores antialiasing automatically | Highest cost; vendor lock-in |
When using a hosted service, keep the same determinism rules and document the service choice in the prototype's README.md.
Anti-Patterns
| Pattern | Why | Fix |
|---|---|---|
| Snapshotting full pages with live data | Diffs every run | Mask or seed data |
| Mixing OS baselines in one folder | Renders differ per OS | One OS, one folder |
Using toMatchSnapshot for visual diffs |
That is for serializers, not images | Use toHaveScreenshot |
| Updating baselines without review | Hides real regressions | Treat baselines like code |
| Skipping the animation guard | Random flakes | Apply vr-mode class + Playwright option |
| Snapshotting one viewport only | Misses responsive bugs | Run mobile / tablet / desktop projects |
Reporting
The prototype-audit pass that consumes this skill writes to docs/artifacts/reviews/PROTOTYPE-AUDIT-<issue>.md:
## Pass 8: Visual Regression
- Status: PASS | FIXED | BLOCKED
- Routes covered: <list>
- Diff results: <n routes>/<n total> within threshold
- Baseline updates: <count> (link to commit)
- Findings:
- <route> @ <viewport>: <maxDiffPixelRatio>; cause: <text>; resolution: <text or escalation>
- Verification: replay run after fixes
Done Criteria
- Baselines exist for every primary route at mobile, tablet, desktop.
- All routes pass under the default threshold or have an explicit, reviewed baseline update.
- Determinism rules applied (animations off, fonts ready, volatile content masked).
- CI runs the suite on every PR that touches the prototype.
Skills to Compose With
development/browser-automation-- shares the Playwright runtimedesign/accessibility-- complementary: axe covers a11y, visual covers layoutdesign/prototype-audit-- this skill is the visual regression passtesting/e2e-testing-- broader Playwright patterns