Set up visual testing in Vitest browser mode
Add the @uiverify/vitest plugin to your browser-mode suite, upload the archive, and wire CI so every pull request gets a screenshot diff. 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 capture and upload commands 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 pull request is checked and every merge to my default branch updates the baseline (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. Keep both of the workflow's triggers: pull_request, and push to my default branch (change [main] if mine is named differently). The push run is required: without it every pull request sees every story as new and renders all of them. Keep --auto-accept-changes on the upload too. It only acts on that push run, so each merge becomes the baseline without a second review. If this repo is public and takes pull requests from forks, use the two-workflow setup from https://uiverify.ai/docs/open-source-and-forks instead, so fork pull requests get the check too without exposing the key.
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.
toMatchScreenshot() and Chromatic, see Vitest visual regression testing.@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 or 5 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; a test that calls takeSnapshot() archives those named snapshots instead.
npm i -D @uiverify/vitest@1.6.0 @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; a test with no takeSnapshot() is archived at its 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. To confirm a font is there, check that (await document.fonts.load('16px "bootstrap-icons"')).length is above 0: it is 0 when no @font-face declares the family, which is the missing-icon symptom exactly. Don't use document.fonts.check() for this, because it returns true for a family that was never declared.// 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.8.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.8.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: its builds are the baseline every PR compares against
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.8.0 upload --static-dir ./uiverify-archive \
${{ github.event_name == 'push' && '--auto-accept-changes' || '' }}
env:
UIVERIFY_API_KEY: ${{ secrets.UIVERIFY_API_KEY }}push trigger on your default branch is required, not optional. Builds there keep the baseline every PR compares against up to date, and they are what lets a PR render only the stories it touches. A workflow that runs only on pull requests sees every story as new on every PR and renders all of them. --auto-accept-changes applies only to that push run: it accepts what you merged, so the baseline moves forward without a second review, while pull requests still wait for yours. Change [main] to whatever your default branch is (master, develop), or the trigger never fires.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.playwright or @playwright/test version in your lockfile) new enough for your CI machine's OS. Each Playwright release installs browsers only on the OS versions it knows, so when your runner image moves to a newer OS, an older Playwright fails at the install step with Playwright does not support chromium on <os> (Ubuntu 26.04, for example, needs 1.61 or newer). Upgrade Playwright, or pin the runner to an OS version it supports. See Troubleshooting.@uiverify/vitest writes the module graph into the archive, and the upload skips unchanged tests by default - see Skip unchanged stories and tests. To skip the job entirely on pull requests that cannot 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. See Triage visual changes from your coding agent.
claude mcp add --scope project --transport http uiverify \
https://uiverify.ai/mcpThen log in once: restart Claude Code, run /mcp, pick uiverify, and log in to UI Verify in your browser. After that the agent can reach every project in every team you are a member of. Project scope writes the server to your committed .mcp.json, so the whole team gets it, and the file holds no secret: each teammate logs in with their own account. Using Cursor, Codex, or another client? See UI Verify MCP server for coding agents for its setup.
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 already took a snapshot isn't captured a second time. A test that fails is still captured, so its UI shows up in the build as a change instead of dropping out of it.
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 />);
});Visual tests for whole pages
Vitest browser mode can capture a whole page, not only a single component. Render the page's own component inside the same layouts and providers your app wraps it in (router, theme, data client), and give it fixture data instead of a live backend. The capture then shows the page the way users see it, with its header, sidebar and layout, and the test needs no running app, dev server or database.
Replace only the boundary, once, in your setup file, and keep everything inside it real:
- Data. Answer your API calls (REST, GraphQL, tRPC) from per-test fixtures with a stubbed
fetchor a mock of your data module, and fail loudly on a call a test has no fixture for. - The request. The current path, search params, cookies and signed-in user, read from a small per-test object by your router and auth doubles.
- What only runs on a server. Server-only modules such as database clients, and any framework API that reads the incoming request.
Before each capture, wait for everything the page loads, not only the part the test asserts on: no request still in flight, and no layout or scroll still moving. Load the fonts and global CSS your app's entry loads, and when the tests run from a different package than the app, add Tailwind @source lines for the app's folders so every class the page uses is generated.
Content a fixture cannot pin, like an embedded map or a third-party widget, can be masked instead: add data-uiverify-ignore to its element and it is painted over in every capture. See Ignore regions.
Next.js App Router pages
In Next.js, import the route's page.tsx, wrap it in its layout.tsx files, and render it inside <Suspense>: React 19 renders async server components in the browser. Replace server-only, next/headers, next/cache, next/navigation and next/link in the setup file, plus the data functions the route files call on the server, and load the fonts next/font provides in the app. The Deterministic Vitest captures skill walks a coding agent through the whole setup.
How is this different from Vitest's built-in toMatchScreenshot?
Vitest's toMatchScreenshot screenshots on the machine that runs the test and diffs against PNGs committed in __screenshots__/, one set per operating system. UI Verify replays an archive of each test in one pinned browser, keeps baselines off your repo, re-renders only what a change touches, and puts the diff on the pull request for you or your coding agent to review. The full comparison, including Chromatic for Vitest, is on Vitest visual regression testing.
Visual testing for agents
UI Verify is visual regression testing for Playwright, Storybook, Vitest and Bun. Your coding agent sees what its UI change did and fixes it before it ships. See how it works.
Get started