Skip to main content

Visual Regression Testing with Vitest

High-level pointers for setting up visual regression testing with Vitest v4's browser mode. This is not a full tutorial โ€” the official Vitest guide is the source of truth; this page covers the key decisions and caveats. See the Vitest tech-stack page for what Vitest is and why we use it, and the Testing Strategy for where visual regression fits in the test pyramid.

What visual regression tests coverโ€‹

Visual regression tests catch unintended UI changes by comparing screenshots of components or pages against a stored baseline. A pixel diff that exceeds a threshold fails the test, surfacing broken layouts, colour shifts, or missing elements that functional assertions would miss.

How it works in Vitest v4โ€‹

Vitest v4 added built-in visual regression testing to its browser mode, which runs tests inside real browsers instead of a simulated DOM, making screenshot-based assertions practical โ€” keeping them consistent across environments is the main caveat (see below).

  • Install vitest and @vitest/browser-playwright (plus vitest-browser-react for component rendering), then import playwright from @vitest/browser-playwright and set provider: playwright() in your config. The default preview provider uses simulated events and is discouraged for real visual testing โ€” Playwright (or WebdriverIO) is required for screenshots and CI.
  • Use the built-in toMatchScreenshot() matcher on a locator to capture and compare renders against a stored baseline.
  • Tests run against real browser engines โ€” Chromium, Firefox, and WebKit โ€” via the Playwright provider, without needing a separate E2E harness.
src/components/Badge.browser.test.tsx
import { render } from 'vitest-browser-react';
import { expect, test } from 'vitest';
import { Badge } from './Badge';

test('Badge renders correctly', async () => {
const screen = render(<Badge label="New" variant="success" />);
await expect(screen.getByText('New')).toMatchScreenshot('badge-success');
});

toMatchScreenshot(name?, options?) accepts an optional name plus comparator options (comparatorOptions.threshold, comparatorOptions.allowedMismatchedPixelRatio) and screenshotOptions (e.g. Playwright mask for dynamic content) โ€” see the official guide for the full API.

note
  • Baselines live in __screenshots__ folders next to the test files, named <test-name>-<browser>-<platform>.png โ€” commit these.
  • Actual/diff artifacts from failures go to .vitest/attachments/ โ€” these are transient, so gitignore them.
  • The first run fails by design: a new reference screenshot is created and must be reviewed before re-running.
  • Update baselines intentionally with vitest --update (-u) when a UI change is deliberate.
  • Stale screenshots for deleted or renamed tests are not cleaned up automatically.

Keeping screenshots stableโ€‹

  • CI/OS consistency is the big caveat. Screenshots differ across operating systems and browser builds (fonts, antialiasing), so baselines generated on one platform won't match another.
  • Flakiness mitigations:
    • The Playwright provider disables animations by default (animations: 'disabled').
    • Vitest retakes screenshots until the page stabilizes (built-in stability detection).
    • Wait for document.fonts.ready before asserting.
    • Mask or mock dynamic content (timestamps, user data) via screenshotOptions.mask.
    • Set an explicit viewport.
caution

Never run --update locally if CI runs on a different OS. The official guide recommends running visual tests in Docker, using a CI-only workflow (e.g. a manually-triggered GitHub Actions job that regenerates baselines on Ubuntu), or a cloud service.

When to use Playwright insteadโ€‹

Playwright also supports screenshot assertions and is the better choice for full-page or flow-level visual checks within an E2E test. Vitest browser mode visual regression is best suited for isolated component snapshots during development. See the Playwright E2E Recipe for setting up flow-level tests.

Resourcesโ€‹