Vitest visual regression testing
Turn your Vitest browser-mode component tests into visual regression tests: capture a DOM archive of each render and diff it on every pull request. No Storybook.
Set it up with your coding agent
Paste this into Claude Code, Cursor, or any coding agent and it wires up UI Verify from these docs.
Set up UI Verify visual regression testing for my Vitest project. Follow https://uiverify.ai/docs/quickstart-vitest to install the packages and add the config. https://uiverify.ai/llms-full.txt has the same docs as one plain-text reference if you want to read them without fetching each page. Then run one build and upload it right now, locally - no CI or GitHub App needed for this - so my first build lands in the dashboard and we can confirm the setup works before wiring anything else. Use the upload command from the quickstart. Open the build page it prints in my browser so I can see the captured stories. Ask me for my UIVERIFY_API_KEY when you need it for the upload. Install the UI Verify skills for this project - the making-ui-changes playbook, the economical-visual-tests, triage, and check-visual-changes playbooks, plus the deterministic-capture guide for this framework - so you can change UI safely, author cheap stable stories, preview an edit's visual impact before pushing, and review builds yourself: npx skills add uiverify/uiverify \ --skill making-ui-changes \ --skill economical-visual-tests \ --skill triage-visual-changes \ --skill check-visual-changes \ --skill vitest-visual-testing Then add a short rule to my AGENTS.md and CLAUDE.md so you read the making-ui-changes skill before any UI change from now on: `Before changing any component, page, or styles, read the making-ui-changes skill and follow it - reuse before you create, add/update the story or capture in the same change, check the blast radius on shared components, and prove the change with a visual test.` After installing, restart this session (or reload the window) so the new skills load - you will not have them until I do. Once that first build is in and looks right, wire the GitHub Actions workflow from the quickstart so every push is checked (add UIVERIFY_API_KEY as a repository secret), and remind me to install the UI Verify GitHub App from my setup page so the check and PR comment post - that's the one step only I can do, and it isn't needed for the first upload.
You do not need Storybook to visually test your components. If you already run component tests in Vitest browser mode, @uiverify/vitest records a DOM archive of each rendered test, and UI Verify replays and screenshots that archive deterministically. It is the same archive-replay engine our Playwright integration uses, and it matches Chromatic's Vitest integration.
@uiverify/vitest into a small component suite with this exact config.1. Get a project API key
Sign up at uiverify.ai, create a Vitest project, and store its API key in CI as UIVERIFY_API_KEY.
2. Add the capture plugin
Install the SDK and add uiverifyPlugin() to your vitest.config.ts. Your tests must run in Vitest 4 browser mode on the Playwright provider (@vitest/browser-playwright) with Chromium - that is where the real DOM to capture exists. Every browser-mode test then archives its final DOM; add named mid-test checkpoints with takeSnapshot().
npm i -D @uiverify/vitest@1.2.1 @vitest/browser-playwrightimport { defineConfig } from "vitest/config";
import { playwright } from "@vitest/browser-playwright";
import { uiverifyPlugin } from "@uiverify/vitest/plugin";
export default defineConfig({
plugins: [uiverifyPlugin()],
test: {
browser: { enabled: true, provider: playwright(), instances: [{ browser: "chromium" }] },
},
});import { test } from "vitest";
import { render } from "vitest-browser-react"; // or your framework's browser render helper
import { takeSnapshot } from "@uiverify/vitest";
test("menu", async () => {
await render(<Menu />); // render() is async - await it so the DOM is committed before capture
await takeSnapshot("closed"); // optional named checkpoint
// final state is auto-archived at test end
});bootstrap-icons, a web font), import it once in a Vitest setup file. A browser-mode test renders an isolated component and never runs your app's entry, so anything only imported there is absent and the capture shows fallback text or empty icon glyphs. UI Verify waits for fonts, but only the ones your test actually loads: it cannot pull in a stylesheet you never imported. document.fonts.check('16px "bootstrap-icons"') stays false until the CSS is imported, which is the missing-icon symptom exactly.// Add this file to test.setupFiles in vitest.config.ts. It runs in the browser
// before each test, in the same document your components render into.
import "bootstrap-icons/font/bootstrap-icons.css"; // an icon font
import "./src/styles/globals.css"; // your global CSS and base fontImport fonts from an npm package or a same-origin asset rather than a CDN <link>, so the bundler serves the bytes and the captured archive stays self-contained. Loading them once in the setup file also keeps the CSS and font imports out of every individual test.
3. Run your tests and upload the archive
Run your Vitest suite as usual. Each test writes what it rendered into ./uiverify-archive; then upload that directory. playwright install --with-deps downloads the browsers Vitest browser mode drives.
npx playwright install --with-deps # one-time: browsers for Vitest browser mode
npx vitest run # writes ./uiverify-archive
UIVERIFY_API_KEY=your_key npx -y uiverify@1.6.0 upload --static-dir ./uiverify-archive4. Catch a change locally
You do not need CI or a pull request to see a diff. Change something visible in a component - a color, a label - then re-run your Vitest browser-mode tests and upload again on the same commit. UI Verify diffs the new build against your first one and flags what moved, so you see the review flow before wiring anything up.
npx vitest run
UIVERIFY_API_KEY=your_key npx -y uiverify@1.6.0 upload --static-dir ./uiverify-archiveOpen the build page it prints: the changed story is waiting in the review queue, where you accept or reject it. Accepting promotes the new screenshot to the baseline for that branch.
5. Wire it into CI
name: UI Verify
on:
push:
branches: [main] # your default branch, so the first build seeds the baseline
pull_request:
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # full history so the baseline can be resolved
- run: npm ci
- run: npx playwright install --with-deps
- run: npx vitest run
- run: npx -y uiverify@1.6.0 upload --static-dir ./uiverify-archive
env:
UIVERIFY_API_KEY: ${{ secrets.UIVERIFY_API_KEY }}push trigger on your default branch matters as much as pull_request: it runs the job once on your default branch so the first build becomes the baseline every PR diffs against. Change [main] to whatever your default branch is (master, develop), or a repo whose default is not main gets a trigger that never fires. Accept that first build once, or add --auto-accept-changes to the upload on your default branch to seed it automatically. Without it, every PR shows all-new until a merge happens.npx playwright install --with-deps is required on a clean runner: Vitest browser mode drives Playwright's Chromium, so without the browsers the test run fails before it captures anything. And keep fetch-depth: 0 so the baseline resolves against your real branch history.--only-changed to the upload so only the tests your change affects re-render - see Skip unchanged stories - and scope the workflow to the PRs that can change the UI - see Run visual tests only on relevant pull requests.UI Verify: <slug> a required status check, or add the auto-rerun workflow that clears the job the moment you accept. Both are in Require the check and auto-clear the CI job.6. Install the visual-testing skills
Give your coding agent the visual-testing playbooks so it can do the work in your repo: triage a build's changes, write economical visual tests, check an edit's visual impact before pushing, and keep captures deterministic. This installs just these skills, not the whole bundle. Restart the agent session afterward so they load.
npx skills add uiverify/uiverify \
--skill making-ui-changes \
--skill economical-visual-tests \
--skill triage-visual-changes \
--skill check-visual-changes \
--skill vitest-visual-testing7. Review changes from your coding agent
Connect the UI Verify MCP and your agent can pull a build's changes into the conversation, look at each diff, read the AI judge's verdict, and accept the intended ones. Your project setup page has this command with the key already filled in; the header is your UIVERIFY_API_KEY. See Triage visual changes from your coding agent.
claude mcp add --scope project --transport http uiverify https://uiverify.ai/api/mcp \
--header 'Authorization: Bearer ${UIVERIFY_API_KEY}'Project scope writes it to your committed .mcp.json, so the whole team gets it. The key is referenced as the UIVERIFY_API_KEY env var (single-quoted so your shell does not expand it at add time), not baked in, so the committed file never holds the secret: each teammate sets UIVERIFY_API_KEY in their environment and Claude Code expands it at runtime.
8. Install the GitHub App
Install the UI Verify GitHub App from your project's setup page and point it at your repo, so it can post a check and a comment on each pull request. This is the one step your coding agent cannot do for you. It does not block getting started: your first upload and baselines work without it, so add it when you want the results to show up on GitHub.
Capture every test, or only some
By default every Vitest browser-mode test archives its final DOM, so vitest run captures your whole browser-mode suite. @uiverify/vitest has no file filter of its own: you scope capture with Vitest itself, the same way you scope a test run. To capture only files like *.visual.test.tsx, point Vitest at them with an include glob (or a separate project you run with --project).
export default defineConfig({
plugins: [uiverifyPlugin()],
test: {
include: ["**/*.visual.test.tsx"], // only these files render and archive
browser: { enabled: true, provider: playwright(), instances: [{ browser: "chromium" }] },
},
});For finer control, keep capture on for the suite and opt individual tests out with disableAutoSnapshot(). Or flip it around: pass uiverifyPlugin({ disableAutoSnapshot: true }) to archive nothing automatically and capture only where a test calls takeSnapshot(). A test that fails, or that already took a snapshot, is never captured twice.
import { test } from "vitest";
import { disableAutoSnapshot } from "@uiverify/vitest";
test("internal only, never screenshotted", async () => {
disableAutoSnapshot(); // opt this one test out of capture
await render(<Debug />);
});How is this different from Vitest's built-in toMatchScreenshot?
Vitest 4 ships its own visual assertion, toMatchScreenshot: it screenshots the element and diffs it against a PNG committed in __screenshots__/. UI Verify runs the same pixel comparison, but over an archive your test captures once, and moves the baselines, the history, the review, and the judgment off your repo and CI. What changes:
- The render is consistent across machines.
toMatchScreenshotrasterizes wherever the test runs, so the same component produces different pixels on your Mac and the CI Linux box, and the baseline PNG is keyed to the OS (-chromium-darwin,-chromium-linux) - teams pin a Docker image to keep them in sync. UI Verify replays every capture in one fixed environment, so the pixels match no matter which machine ran the test. You still freeze in-app non-determinism - the clock, live data, a feature flag - in the test either way; the determinism checklist shows how. - No screenshot images in your repo. Every native baseline is a binary PNG committed to the repo, so it bloats history and merge-conflicts when two branches touch the same story. UI Verify stores one baseline per branch, resolved from git, with nothing in your repo.
- Full history of every component, not just the latest PNG. UI Verify keeps every version of a story, so you can see exactly how a component looked build over build and when it changed. A committed baseline holds only the current image, and git cannot visually diff a binary, so with
toMatchScreenshotthat history is effectively gone. See baselines. - It re-renders only what changed.
toMatchScreenshotre-screenshots and re-diffs every test on every run, inside your CI. UI Verify captures the DOM and re-renders only the stories your pull request touched, on its own fleet, carrying the rest forward - so the pixel work, and the bill, scale with your change and not with the size of your suite. See skip unchanged. - The diff is on the pull request, not a file on disk. A native failure writes the expected, actual, and diff images to disk; to see them from a CI run you download the run's artifacts and open them by hand. UI Verify posts a check and the diff on the PR, with a review queue where a change is something you accept or reject.
- Updating a baseline is a click, and merging advances it for you. With
toMatchScreenshotyou regenerate the PNG on the right OS with--update-snapshotsand commit it. UI Verify promotes the baseline for the branch when the pull request merges, through a GitHub webhook - nothing to regenerate, nothing to commit. - An AI judge and agent review. UI Verify labels each change a regression or an intended change and tells you what moved, not just that N pixels differ, and it exposes the cropped before-and-after over MCP, so your coding agent reviews the pixels it changed without downloading an artifact or leaving the pull request. See the AI judge and, for the same comparison against Playwright's
toHaveScreenshot, UI Verify vs Playwright screenshots. - Flaky changes are auto-ignored, not build-breaking. A change that does not reproduce on a re-render is dropped, so a flaky screenshot does not turn your pull request red and send you re-running the suite. See automatic flake detection.
Visual testing for agents
UI Verify captures your UI on every pull request and an AI judge tells an intended change from a real regression. See how it works.
Get started