Frameworks3 min read

Vitest visual regression testing

Vitest browser mode ships visual comparison natively through toMatchScreenshot. How to set it up, why the ecosystem is moving here from the Storybook test-runner, and the stability problem Vitest is unusually honest about.

VisualMax EngineeringProduct team

Vitest runs visual regression tests out of the box in browser mode, through a toMatchScreenshot assertion. That matters beyond Vitest users, because it is where the component-testing ecosystem is visibly heading: Storybook now recommends its Vitest addon over the older test-runner, Chromatic has previewed Vitest visual testing, and Argos shipped a Vitest SDK.

The basic assertion

ts
import { expect, test } from "vitest";
import { page } from "vitest/browser";

test("hero section looks correct", async () => {
  // ...render or navigate

  await expect(page.getByTestId("hero")).toMatchScreenshot("hero-section");
});

First run writes the reference image and fails the test — deliberately, so a baseline is never silently created by a passing run. Later runs compare against it.

The platform-naming consequence

Like Playwright, Vitest includes the browser and platform in the screenshot name — button-chromium-darwin.png. So a baseline recorded on a developer's Mac is a different file from the one CI compares against on Linux, and updating the first does not update the second.

The fix is the same as everywhere else: record baselines only inside the container CI uses, and make that the sanctioned route.

bash
docker run --rm --network host \
  -v "$PWD:/work" -w /work \
  mcr.microsoft.com/playwright:v1.56.0-noble \
  npx vitest --browser.headless --update

The full set of causes and the order worth fixing them in is in how to fix flaky visual tests — all of it applies here unchanged.

Why the ecosystem is moving here

Three things converged, and if you are following a guide written before 2026 it probably predates all of them.

  1. The Storybook test-runner is superseded. Storybook's documentation now says the test-runner "has been superseded by the Vitest addon, which offers the same functionality, powered by the faster and more modern Vitest browser mode", and recommends the addon for Vite-powered setups.
  2. Browser mode runs real browsers. Component tests execute in an actual browser rather than jsdom, which is the prerequisite for meaningful screenshots — jsdom has no layout engine and cannot render.
  3. Comparison is built in. toMatchScreenshot means no plugin assembly, which is the position Cypress users are still in — see Cypress visual regression testing.

With Storybook

If your components live in Storybook, the Vitest addon runs your stories as browser tests:

bash
npx storybook add @storybook/addon-vitest

The addon runs and asserts; it does not itself diff images. Visual capability comes from a companion addon — storybook-addon-vrt, storybook-addon-vis and @storycap-testrun/browser all exist. This is the fastest-moving corner of the ecosystem, so check recent commit history before adopting one; it has already lost Lost Pixel, archived in April 2026.

Vitest or Playwright?

Vitest browser modePlaywright
Natural scopeComponents, in isolationPages and flows, in a real app
AssertiontoMatchScreenshottoHaveScreenshot
Retry until stableImprovingMature
EnginesVia provider (Playwright or WebdriverIO)Chromium, Firefox, WebKit natively
Storybook integrationFirst-class via the addonPossible, less direct
Best whenYou already run Vitest for unit testsYou already run Playwright end-to-end

In practice this is rarely a choice — you use whichever you already run. Many teams end up with both: Vitest for component-level checks, Playwright for the composed pages. That is a sensible split rather than a duplication, because they cover genuinely different risks.

What it still does not solve

Identical to every other framework-level tool, and worth stating plainly:

  • You author every check. A component with no test is invisible.
  • No hosted review workflow. Approval is a CLI operation and a committed image; a designer cannot weigh in.
  • Baselines live in your repository as binaries, with the platform-naming trap above.
  • Component coverage is not page coverage. A component passing in isolation says nothing about the page composing it.

Those are the trade-offs the hosted tools exist to absorb — compared in 10 visual regression testing tools.

Frequently asked questions

Does Vitest support visual regression testing?

Yes, natively in browser mode through the toMatchScreenshot assertion. It captures a component or page screenshot and compares it against a stored reference. The first run creates the reference and deliberately fails, so baselines are never created silently by a passing test.

Why do Vitest screenshots differ between machines?

Vitest's own documentation lists the causes: GPU and drivers, operating system, font rendering pipelines, browser version and settings, headless versus headed mode, and screen scaling or colour profiles. It names screenshots per browser and platform for this reason and strongly recommends running them in Docker or a cloud service.

Should I use Vitest or Playwright for visual testing?

Usually whichever you already run. Vitest browser mode suits component-level checks and integrates first-class with Storybook through its addon; Playwright suits page and flow coverage in a real application and has more mature retry-until-stable behaviour. Running both for their respective scopes is a reasonable split.

Sources

  1. Vitest — Visual Regression Testing documentationchecked 2026-09-14
  2. Storybook Vitest addon documentationchecked 2026-09-14

Referenced by