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.
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
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.
docker run --rm --network host \
-v "$PWD:/work" -w /work \
mcr.microsoft.com/playwright:v1.56.0-noble \
npx vitest --browser.headless --updateThe 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.
- 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.
- 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.
- Comparison is built in.
toMatchScreenshotmeans 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:
npx storybook add @storybook/addon-vitestThe 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 mode | Playwright | |
|---|---|---|
| Natural scope | Components, in isolation | Pages and flows, in a real app |
| Assertion | toMatchScreenshot | toHaveScreenshot |
| Retry until stable | Improving | Mature |
| Engines | Via provider (Playwright or WebdriverIO) | Chromium, Firefox, WebKit natively |
| Storybook integration | First-class via the addon | Possible, less direct |
| Best when | You already run Vitest for unit tests | You 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
- Vitest — Visual Regression Testing documentationchecked 2026-09-14
- Storybook Vitest addon documentationchecked 2026-09-14