ScreenshotNeo

BlogHow-to

Visual Testing with Vitest: How to Catch UI Regressions

Add screenshot regression checks to Vitest, create and review baselines, reduce flaky diffs, and diagnose failures in a repeatable browser environment.

By the ScreenshotNeo team4 October 202610 min read

Vitest visual regression testing compares a browser screenshot of your UI with a saved reference image. With Vitest 4 or later, enable Browser Mode, choose a browser provider, and use toMatchScreenshot() in a browser test. The comparison catches appearance changes; pair it with behavior assertions to check that the UI still works.

This guide walks through setup, a focused TypeScript test, baseline review, updates, repeatability, tolerances, CI, and common failures. Check the current Vitest guide against your installed version: Browser Mode configuration and provider details can change.

1. Set up Vitest Browser Mode

Browser Mode runs tests in an actual browser and requires a provider. Vitest documents preview, Playwright, and WebdriverIO providers; for CI, install Playwright or WebdriverIO. Vitest recommends Playwright as a starting point if you do not already use one of these tools. Follow the Browser Mode installation guide for your package manager and Vitest version.

Keep visual tests in a separate project or otherwise distinct from ordinary unit tests. That gives visual changes a clear failure signal and lets you choose a deliberate baseline update workflow. The example below uses a vrt project and test filenames ending in .vrt.test.ts. Replace [browser-name] with the browser name supported by the provider you have configured.

// vitest.config.ts
import { defaultExclude, defineConfig } from 'vitest/config'

const vrtPattern = '**/*.vrt.test.[tj]s?(x)'

export default defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          exclude: [vrtPattern, ...defaultExclude],
        },
      },
      {
        test: {
          name: 'vrt',
          include: [vrtPattern],
          browser: {
            headless: true,
            instances: [
              {
                browser: '[browser-name]',
                viewport: { width: 1280, height: 720 },
              },
            ],
          },
        },
      },
    ],
  },
})

Add separate scripts so the visual suite can run on its own. These scripts assume the project configuration above and your provider are ready.

{
  "scripts": {
    "test:unit": "vitest --project unit",
    "test:visual": "vitest --project vrt"
  }
}

2. Write a focused screenshot assertion

Render or navigate to the state you want to protect, choose a stable element, and await toMatchScreenshot(). An explicit screenshot name makes the expected state easier to recognize. Prefer a component or region unless the whole-page composition is what matters.

// tests/button.vrt.test.ts
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'

test('primary button appearance', async () => {
  // Render the component using your app's normal component test setup.
  const button = page.getByRole('button', { name: 'Save changes' })
  await expect(button).toMatchScreenshot('primary-button')
})

The component render step depends on your framework and test setup; the browser imports and screenshot assertion are the Vitest portion. Use accessible roles and names to locate elements where practical, so the selector describes the UI element rather than depending on fragile styling classes.

A visual assertion cannot tell you whether the button submits a form, handles keyboard input, or updates application state correctly. Keep those checks as behavior tests and use the screenshot assertion as complementary appearance coverage. Vitest explains this distinction in its visual regression guide.

3. Create and review the first baseline

  1. Run npm run test:visual.
  2. On the first run, Vitest creates a reference screenshot and reports that no reference existed. This is expected; the first run does not yet compare against a committed image.
  3. Open the generated image and confirm it shows the intended state, at the intended viewport and with the expected data.
  4. Commit the reviewed screenshot with the test. Vitest stores references in __screenshots__ directories beside tests by default.
  5. Run the visual test again. Later runs compare the new capture with that reference.

Vitest includes the test name, browser, and platform in the screenshot filename. Different browser and platform combinations can therefore have separate reference images. Keep the files under version control so reviewers can inspect a baseline change alongside the code that caused it.

4. Update screenshots safely

When a design change intentionally changes appearance, update the references with the Vitest update flow, for example:

vitest --project vrt --update

Review each changed screenshot before committing it. A passing test after an update only means the new capture matches the new reference; it does not establish that the new design is correct. If CI is your standardized rendering environment, generate intentional updates there or otherwise use the same controlled environment. Updating from a different machine can introduce rendering changes unrelated to the design.

Vitest does not automatically remove screenshots for deleted or renamed tests. Remove stale files from the relevant __screenshots__ directory when you clean up or rename tests. For a larger suite, Vitest recommends considering Git LFS for reference images.

5. Reduce flaky screenshot tests

Screenshot output can vary with the browser and version, operating system, fonts, GPU and drivers, viewport, display scaling, color profile, and headless or headed execution. Standardize those conditions between baseline creation and comparison. A shared CI or container environment helps a team compare like with like.

Make the captured state deterministic

  • Control data. Mock API responses and use fixed test data. Avoid random values, current timestamps, rotating content, and account-specific state unless those are what the test covers.
  • Wait for rendering to settle. Vitest’s assertion strategy takes repeated captures and compares consecutive images until the page stabilizes or a timeout is reached. A region that changes continuously can still time out. Wait for loading indicators to disappear and required content to appear.
  • Wait for fonts. If the test depends on web fonts, wait before capture: await document.fonts.ready. Missing or differently rendered fonts can create widespread text diffs.
  • Control animation. With the Playwright provider, Vitest’s built-in screenshot assertion disables animations by default. For other needs, use the documented CSS approach or provider-specific screenshot options. An endlessly animated element can prevent stable captures.
  • Fix viewport dimensions. Set an explicit viewport in the browser instance. If testing responsive layouts, make separate tests or browser instances for the viewports that matter, especially around breakpoints.
  • Capture narrowly. A component screenshot avoids unrelated page changes. Capture the full page only when page-level composition is the intended contract.
  • Mask only genuinely volatile regions. With the Playwright provider, the documented screenshotOptions.mask option can mask dynamic elements. Prefer deterministic test data when possible; masking can also conceal a real regression in the masked region.

Vitest’s assertion waits for consecutive captures to stabilize, which helps with asynchronous images, fonts, animations, and layout settling. It cannot make a continually changing page deterministic. Fix the source of ongoing variation or exclude only the specific volatile region that is outside the test’s purpose.

6. Configure comparison tolerance

The built-in comparator is pixelmatch. You can set global defaults in the browser assertion configuration or pass comparator options to a particular assertion. The example shows the option shape documented by Vitest:

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    browser: {
      expect: {
        toMatchScreenshot: {
          comparatorName: 'pixelmatch',
          comparatorOptions: {
            threshold: 0.2,
            allowedMismatchedPixelRatio: 0.01,
          },
        },
      },
    },
  },
})

Or tune an individual assertion:

await expect(button).toMatchScreenshot('primary-button', {
  comparatorName: 'pixelmatch',
  comparatorOptions: {
    allowedMismatchedPixelRatio: 0.01,
  },
})

threshold controls how different colors can be for a pixel to count as different; allowedMismatchedPixelRatio limits the proportion of pixels that may differ. Vitest also documents an absolute allowedMismatchedPixels limit. If you set both an absolute limit and a ratio, the stricter limit applies. A ratio scales with screenshot dimensions; an absolute pixel count does not.

There is no universal correct tolerance. Begin with a controlled rendering environment and a strict comparison, inspect the actual noise, and choose the smallest tolerance that handles that observed variation while still surfacing meaningful changes. Do not raise the threshold simply to make a failing test pass. A perceptual comparator can be registered through Vitest’s comparator API if pixel matching remains too noisy, but it changes what counts as a regression; use it only when that tradeoff suits the UI.

7. Run visual tests in CI

Run the same visual project in a pinned, repeatable environment. CI runners may not have browsers installed, so install the browser required by your provider before running the suite. For Playwright, Vitest’s guide shows this GitHub Actions step:

- name: Install Playwright browsers
  run: npx --no playwright install --with-deps --only-shell

- name: Run visual tests
  run: npm run test:visual

Pin Vitest, the provider, and browser versions where your project’s dependency and CI setup allows it. Keep viewport and operating-system conditions consistent with the reference-generation workflow. Treat baseline updates as a reviewed change, not as an automatic response to every mismatch: a mismatch may indicate a real defect, an intentional design change, or environmental noise.

8. Read a failed comparison

Vitest can provide three images: the saved reference, the current capture, and a diff. The diff is available when the compared images have matching dimensions. Use all three to determine whether the UI changed, the screenshot size changed, or the environment rendered it differently.

  1. Check that reference and actual images have the same dimensions and intended viewport.
  2. Compare the reference and actual images to identify the first changed region.
  3. Inspect the diff to see whether the change is broad, localized, or mostly around text edges.
  4. Check data, fonts, loading state, animation, browser version, and operating system before changing tolerance.
  5. If the change is intentional, update and review the baseline. If it is a defect, fix the UI and keep the reference.

A broad diff can mean a substantial layout or state change. Fine differences near glyph edges may come from rendering variation, but investigate them in the standardized environment before deciding they are acceptable noise. A screenshot shows what rendered; it does not explain why.

9. Common errors and fixes

Symptom Likely cause What to do
Browser APIs or toMatchScreenshot are unavailable The test is not running in Browser Mode, the provider is missing, or the installed Vitest version does not support the documented visual assertion. Check the installed Vitest version and follow its current Browser Mode installation guide. Run this test under the browser project and import page from vitest/browser.
First run fails because no reference exists The baseline has not been created yet. Inspect the generated reference, then rerun and commit the approved image.
Every run differs on a developer’s machine Browser, OS, fonts, GPU, scaling, or execution mode differs from the baseline environment. Compare and update in one standardized environment; set the viewport explicitly and align browser versions.
Text-only diffs appear A font is missing, still loading, or rendered differently. Wait for document.fonts.ready, ensure the expected font is available, and compare on the shared environment. Tune tolerance only after measuring the remaining noise.
Intermittent diffs or timeouts Data or layout is still changing, content loads late, or animation never settles. Use deterministic data, wait for the relevant state, disable unwanted animations, and remove the ongoing change. Increase a timeout only when the stable capture legitimately takes longer.
Diff image is missing The reference and actual screenshots have different dimensions. Check viewport, responsive breakpoint, and capture scope. Compare the two source images directly.
Updated baselines hide a bug The update was accepted without reviewing the new image. Restore or correct the baseline, inspect each changed image, and commit only intentional visual changes.
Old screenshot files remain after test cleanup Vitest does not automatically remove references for deleted or renamed tests. Delete stale files manually from the associated __screenshots__ directory.

10. Performance, reliability, and storage

Every visual assertion requires browser rendering and image comparison, so a visual suite adds work beyond ordinary unit assertions. Keep the suite focused on high-value states, capture components when that matches the requirement, and run the visual project independently when that makes local feedback more useful. Large full-page images and many viewport variants also increase the amount of screenshot data your repository must manage.

Reliability depends more on repeatable rendering and controlled state than on making the comparator permissive. Use a shared environment, fixed data, explicit viewport sizes, and reviewed references. If reference images make a large repository cumbersome, Vitest suggests considering Git LFS. The project must weigh the added browser setup, execution time, and image storage against the value of catching visual changes in its own UI.

Or skip the browser setup

Vitest is a good fit when you want visual assertions next to your tests and control over the browser environment. If your immediate task is to capture a website without setting up browser automation, ScreenshotNeo provides a website screenshot API and MCP server. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const image = await res.arrayBuffer();
  • Cookie banners are accepted like a visitor would accept them, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot. Each step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • The Free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.

Sign up free for 1,000 screenshots a month with no card.

FAQ

Does Vitest visual testing work with ordinary Node tests?

The documented toMatchScreenshot() flow uses Browser Mode. Configure a browser provider and run the test in that browser project.

Should every page get a screenshot test?

No. Protect important visual states where a screenshot adds useful coverage. Focused component captures reduce unrelated diffs; use full-page captures when the complete composition is what you need to preserve.

Can screenshot tests prove an interaction works?

No. A screenshot records appearance. Add behavior assertions for actions, keyboard use, state changes, and other requirements.

Can I use different viewports or browsers?

Yes. Configure the browser instances and viewport sizes that matter to your supported UI. Each environment can require its own reference image, so keep the matrix intentional and consistent.

References