Skip to content

Visual Regression Testing

A visual regression test captures a rendered UI as an image and compares it to a baseline from a previous build. Any unexpected pixel difference — a shifted button, a broken font, a CSS leak — is flagged for review.

Some tools compare pixels directly; others (Applitools) model the page like a human eye and use AI diffing. Either way the workflow is the same: capture → compare → review → accept or reject.

baseline.png new render
│ │
└────────┬───────────┘
▼
diff engine
│
┌────────┴─────────┐
▼ ▼
within tolerance beyond threshold
(pass) (fail → review diff)

Visual tests complement functional tests: a green E2E run can still ship a layout that is visually broken.

Use it for

  • Design systems and shared component libraries
  • Responsive layouts across breakpoints and browsers
  • Marketing pages, themes, dark mode, localisation where layout is the product
  • Catching CSS regressions that DOM assertions cannot see

Avoid it when

  • Content changes constantly (news feeds, live data) — use masking or skip
  • There is no stable rendering environment, so results flake on antialiasing
  • You test every page pixel-by-pixel instead of a small, curated set
  • You have no review workflow, so diffs pile up and get rubber-stamped
Tool Ecosystem Notes
Playwright toHaveScreenshot JS/TS Built in; no account required; runs locally and in CI
Chromatic JS/TS Storybook-native, cloud review UI, TurboSnap
Percy JS/TS, multi Cloud baselines, cross-browser rendering
Applitools Eyes Multi AI visual diffing, cross-device viewports
BackstopJS JS/TS Open-source, Puppeteer/Playwright based, local baselines
reg-suit JS/TS OSS CLI, stores baselines in your own storage
Loki JS/TS Storybook screenshot testing with Chromatic-like review
Terminal window
npm install -D @playwright/test
npx playwright install --with-deps
npx playwright test --update-snapshots # create baselines
npx playwright test # compare
import { test, expect } from '@playwright/test';
test('pricing page looks correct', async ({ page }) => {
await page.goto('/pricing');
await expect(page).toHaveScreenshot('pricing.png', {
maxDiffPixelRatio: 0.01,
mask: [page.getByTestId('live-counter')],
});
});
  • Pin the rendering environment. Same OS, browser version, fonts and device scale factor across baseline and CI, or every diff is noise.
  • Mask or freeze dynamic regions. Clocks, avatars, ads and live counters should be masked so only real changes surface.
  • Test a curated set, not every page. Focus on shared components and high-traffic layouts; a giant matrix just multiplies flaky diffs.
  • Always review diffs, never auto-accept. A visual review gate is the whole point; blanket approval defeats it.
  • Set an explicit tolerance. maxDiffPixelRatio/thresholds absorb antialiasing noise while still catching real layout breaks.
  • Run visual tests in CI on a fixed viewport list. Reproduce results headlessly and deterministically, not on a developer laptop.

Self-assessment quiz

Pick one answer per question, then press Check answers.

  1. What does a visual regression test detect?
  2. Why must the rendering environment be pinned?
  3. What is the purpose of masking dynamic regions?
  4. How does Applitools differ from a plain pixel-diff tool?
  5. Which built-in Playwright API captures a visual baseline?