ESC

Type to search the knowledge base.

Visual Regression Testing

Catch CSS regressions with screenshot diffs — tooling options, what to snapshot, flake from fonts/animations, and review workflows.

intermediate3 min read
  • testing
  • visual-regression

DOM assertions don’t see that a button overflowed or contrast collapsed. Visual regression testing compares screenshots (or DOM layouts) against a baseline and fails on unexpected pixel differences.

Docs: Playwright screenshots, Chromatic, Storybook visual tests.

Approaches

Approach Tools Best for
Component catalog Chromatic, Loki, Storybook + CI Design systems
E2E screenshots Playwright toHaveScreenshot Key pages
Cross-browser pixels Cloud services High-stakes marketing

Playwright example

import { test, expect } from '@playwright/test';

test('pricing page layout', async ({ page }) => {
  await page.goto('/pricing');
  await expect(page).toHaveScreenshot('pricing.png', {
    maxDiffPixelRatio: 0.01,
  });
});

First run creates baseline; later runs diff.

What to capture

Do: design-system states, checkout header, critical landing hero, empty/error components.

Don’t: entire app every commit, pages full of live charts with random data, ads.

Stabilize content:

await page.clock.install(); // if available
await page.route('**/analytics/**', (r) => r.abort());

Mask dynamic regions:

await expect(page).toHaveScreenshot({
  mask: [page.getByTestId('live-clock')],
});

Flake sources

  1. Fonts not loaded → use waitForFunction / network idle carefully.
  2. Animations — disable via CSS in test:
*, *::before, *::after {
  animation: none !important;
  transition: none !important;
  caret-color: transparent !important;
}
  1. Subpixel / OS differences — run in Docker or vendor cloud for consistent baselines.
  2. Anti-aliasing — allow small maxDiffPixelRatio.

Review workflow

Visual diffs need human approval. Treat baseline updates like code review:

  • PR shows image diff
  • Designer/FE approves intentional changes
  • Never auto-accept on main without eyes

Relation to other tests

Layer Catches
Unit/RTL Behavior, a11y roles
Visual Spacing, color, overflow
E2E functional Flows still work

Visual green ≠ accessible. Still run axe and keyboard checks.

Footguns

  1. Huge snapshot surface → constant noise.
  2. No masking of user avatars/timestamps.
  3. Different CI runners than local without containerization.
  4. Using visual tests instead of fixing layout with better CSS tests for simple cases.

Interview out-loud answer

“Visual regression compares screenshots to baselines for CSS bugs. I snapshot critical components/pages, disable animation, mask dynamic regions, and review diffs in PRs. It’s complementary to Testing Library, not a replacement.”

Threshold tuning

Start strict (maxDiffPixelRatio low) on design-system components and slightly looser on full pages. If thresholds rise every week, the suite is absorbing real bugs — reset baselines after intentional redesigns only.

Extra practice

Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.

Notes from real codebases

Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.

Further reading

Related guides