# UI Verify documentation > Visual regression testing for agent-written UI, and an alternative to Chromatic and Percy. The full text of every documentation page follows. # What is visual regression testing? Visual regression testing catches unintended UI changes by diffing a screenshot of your UI against a known-good baseline, pixel by pixel. Source: https://uiverify.ai/docs/what-is-visual-testing Unit tests prove your logic is correct, but they never see the UI. A refactor, a one-line CSS tweak, or a dependency bump can shift a layout, push a button off-screen, or break dark mode - and every assertion still passes green. Visual testing is the check for exactly that: the pixels, not the logic. ## How visual testing works 1. Render each component or page in a known, repeatable state. 2. Capture a screenshot of that render. 3. Diff the screenshot against the stored baseline for that state. 4. Review the changes - a human or an AI judge accepts or rejects each one. 5. Accepted screenshots become the new baseline the next run is compared against. ## Why visual testing matters The failure it catches is the one that spans your whole app. Edit a design token, a shared `Button`, or a global stylesheet and the effect ripples across dozens of screens at once. A unit test on the checkout button still passes while the button is now white text on a white background. Visual testing is the only test that would have seen it. - Catches CSS and layout regressions that logic tests structurally cannot see. - Covers cross-cutting changes - design tokens, shared components, dependency bumps - in one pass. - Turns an intended UI change into a reviewable diff, so it is documented, not discovered in production. - Protects dark mode, responsive breakpoints, and states that are tedious to check by hand. ## Visual testing for agent-written UI Coding agents now write a lot of UI, and they move fast and touch shared components confidently. The bottleneck has shifted: the hard part is no longer writing the code, it is verifying the pixels the agent produced. Visual testing gives every agent pull request a screenshot diff, and [an AI judge](/docs/the-ai-judge) that separates the change the agent meant to make from the one it did not. ## Visual testing vs snapshot testing vs end-to-end - **Snapshot tests** serialize the DOM or a component tree to text. They break on trivial markup noise and never see a rendered pixel - a wrong color passes. - **End-to-end tests** (Playwright, Cypress) prove a flow works, but they rarely assert appearance - a broken layout still clicks through. - **Visual tests** assert the rendered pixels themselves, which is the one thing the other two miss. > Ready to set it up? Start with the [Storybook quickstart](/docs/quickstart-storybook) or the [Playwright quickstart](/docs/quickstart-playwright). --- # Storybook visual regression testing Add visual regression testing to Storybook in minutes: install the uiverify CLI, build your stories, and get a screenshot diff check on every pull request. Source: https://uiverify.ai/docs/quickstart-storybook If you already have Storybook, you already have your capture targets: every story is a state worth screenshotting. UI Verify renders each one, captures it, and diffs it against the baseline. Here is the whole setup. ## 1. Get a project API key Sign up at [uiverify.ai](/), create a project, and copy its API key. Store it in your CI as a secret named `UIVERIFY_API_KEY`. ## 2. Build your Storybook ```bash npm run build-storybook # outputs ./storybook-static ``` ## 3. Upload it Run this locally to establish your first baselines. Pass the API key inline (or export it first) - the CLI reads it from the environment. ```bash UIVERIFY_API_KEY=your_key npx -y uiverify@latest upload --static-dir ./storybook-static ``` The first run establishes your baselines. Every run after it diffs against them and reports what moved. ## 4. Install the GitHub App Install the GitHub App from your project settings and point it at your repo. This is what lets UI Verify post a check and a comment on each pull request. Uploads still work without it, but nothing shows up on GitHub, so do this before you wire CI up. ## 5. Wire it into CI ```yaml name: UI Verify on: pull_request jobs: visual: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full history so the baseline can be resolved - run: npm ci && npm run build-storybook - run: npx -y uiverify@latest upload --static-dir ./storybook-static env: UIVERIFY_API_KEY: ${{ secrets.UIVERIFY_API_KEY }} ``` > `fetch-depth: 0` matters: the default shallow checkout hides your branch history, and UI Verify needs it to resolve the right baseline. Without it you get phantom diffs. ## 6. Review changes on the pull request UI Verify posts a check and a comment on the PR. Changed stories go to a review queue where you accept or reject each one; accepting promotes the new screenshot to the baseline for that branch. See [how the review model works](/docs/how-visual-testing-works). ## Render only what changed On a large Storybook, add `--only-changed` to render just the stories your PR affects and carry the rest forward. It needs Storybook's dependency graph, which Storybook only writes when you build with `--stats-json` (`npm run build-storybook -- --stats-json`). It is the single biggest lever on build time and cost - see [Skip unchanged stories](/docs/skip-unchanged). ## Frequently asked questions ### Do I need to write new tests? No. Your existing stories are the tests. If a component has no story yet, add one story that renders it - and prefer a dense [gallery story](/docs/economical-stories) over one story per variant. ### Why does a story come back changed when nothing changed? That is a flaky diff, and it is almost always run-to-run variation the screenshot faithfully captured - a clock, an animation, live data. See [Fix flaky visual tests](/docs/deterministic-captures). --- # Playwright visual regression testing Turn your Playwright tests into visual regression tests: capture a deterministic archive of the real pages they visit and diff it on every pull request. No Storybook. Source: https://uiverify.ai/docs/quickstart-playwright You do not need Storybook to do visual testing. If you already drive real pages with Playwright, `@uiverify/playwright` records an archive of what each page actually rendered, and UI Verify replays and screenshots that archive deterministically. This is how you visually test a Next.js app, a marketing site, or a live staging URL. ## 1. Get a project API key Sign up at [uiverify.ai](/), create a project, and store its API key in CI as `UIVERIFY_API_KEY`. ## 2. Add the capture SDK and swap your import Install the capture SDK, then swap your Playwright import so every test archives its final UI state. This import swap is the step that actually records the archive - installing the package alone does nothing. Add named mid-test checkpoints with `uiVerify.snapshot()`. ```bash npm i -D @uiverify/playwright ``` ```ts // swap @playwright/test -> @uiverify/playwright: every test now also archives its UI import { test, expect } from "@uiverify/playwright"; test("checkout", async ({ page, uiVerify }) => { await page.goto("/cart"); await uiVerify.snapshot("cart"); // optional named mid-test checkpoint await page.click("#checkout"); // final state is auto-archived at test end }); ``` ## 3. Run your tests and upload the archive Run your Playwright suite as usual. Each test writes what it rendered into `./uiverify-archive`; then upload that directory. `playwright install` downloads the browsers once. ```bash npx playwright install # one-time: download the browsers npx playwright test # writes ./uiverify-archive UIVERIFY_API_KEY=your_key npx -y uiverify@latest upload --static-dir ./uiverify-archive ``` ## 4. Install the GitHub App Install the GitHub App from your project settings and point it at your repo, so UI Verify can post a check and a comment on each pull request. Uploads still work without it, but nothing shows up on GitHub. ## 5. Wire it into CI ```yaml name: UI Verify on: pull_request jobs: visual: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full history so the baseline can be resolved - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test - run: npx -y uiverify@latest upload --static-dir ./uiverify-archive env: UIVERIFY_API_KEY: ${{ secrets.UIVERIFY_API_KEY }} ``` > `npx playwright install --with-deps` is required on a clean runner - without the browsers, `playwright test` fails before it captures anything. And keep `fetch-depth: 0` so the baseline resolves against your real branch history. ## Why archive-replay instead of a live screenshot? A screenshot taken against a live page flakes on anything non-deterministic - live data, the clock, feature flags, a third-party widget that loads a beat late. Archive-replay bakes the page into a static archive at record time, then replays it identically on every run, so a diff means a real change and not a coincidence of timing. See [Fix flaky visual tests](/docs/deterministic-captures). > Your coding agent can wire this up for you - the [Playwright visual testing skill](/skills/playwright-visual-testing) packages the fixture setup and the determinism checklist. --- # How visual regression testing works From CI upload to a green check: how UI Verify renders every component in a real browser, diffs it against its baseline, and has an AI judge triage each change. Source: https://uiverify.ai/docs/how-visual-testing-works UI Verify runs as a single step in your CI. You hand it a built Storybook or a Playwright archive; it does the rest in the cloud and reports back on the pull request. Here is what happens between the upload and the check. ## The pipeline 1. Your CI runs `uiverify upload` with your built Storybook or Playwright archive. 2. UI Verify ingests the build and resolves the baseline for each story from its branch history. 3. A fleet of workers renders every story in a real browser and diffs it against its baseline. 4. An AI judge reviews each changed story and labels it a likely regression or a likely intended change, with a reason. 5. A check and a comment land on your PR, with the changes grouped and pre-sorted for review.  ## The review model Baselines are per branch. A change stays a change until a human, or an agent you authorized, accepts it - and accepting is what sets the new baseline. Nothing updates itself behind your back, which is what keeps a green build trustworthy. See [Baselines and branches](/docs/baselines). ## Where it runs Everything after the upload happens in the cloud. There are no browsers to install in CI, no snapshot images committed to your repo, and no local render farm to babysit. Your pipeline stays one step. > The part that makes the review queue short is [the AI judge](/docs/the-ai-judge), which triages every change before you see it. --- # Baselines and branches A baseline is the known-good screenshot every new capture is diffed against. It is chosen per branch and updates only when you accept a change. Source: https://uiverify.ai/docs/baselines ## What is a baseline? A baseline is the accepted screenshot of a story that the next capture is diffed against. A diff only exists relative to a baseline: without one, a story is new, not changed. Establishing baselines is what your very first build does. ## How baselines are chosen Per branch. A pull request's baseline for each story is the most recent accepted screenshot on its ancestor - usually your `main` branch. So your feature branch diffs against `main`, and you see only the changes your branch introduces, not the whole history. ## When does a baseline update? Only when a change is accepted - by you in the dashboard, or by [your agent over MCP](/docs/triage-with-your-agent). Accepting promotes the candidate screenshot to the new baseline for that branch. When the PR merges, its accepted baselines carry forward to `main`, and every branch that later branches off inherits them. ## Why does my PR show a change I didn't make? Nine times out of ten the branch is simply behind. Someone accepted a change on `main` after you branched, so your baseline is older than the latest accepted screenshot. Rebase or merge `main` in and the phantom diff resolves. ## A story I didn't touch is showing a diff - is that a bug? Maybe, and it is worth a look rather than a shrug. If a changed story has nothing to do with your diff, do not dismiss it as flake and do not just re-run. It can mean a baseline moved that should not have - a story carried forward against a screenshot nobody verified. Investigate the unexplained change before you accept it; that habit is the only thing that catches a silently wrong baseline. --- # Skip unchanged stories with --only-changed Render only the stories your pull request affects and carry the rest forward - the biggest lever on build time and per-snapshot cost on a large Storybook. Source: https://uiverify.ai/docs/skip-unchanged On a large Storybook most pull requests touch a handful of components. Re-rendering all 1,000 stories every time is slow, and on any per-snapshot tool it is expensive. `--only-changed` renders just the stories your change can affect and carries the unchanged ones forward from their baseline. ```bash npx -y uiverify@latest upload --static-dir ./storybook-static --only-changed ``` > `--only-changed` needs Storybook's module dependency graph, which Storybook only writes when you build with `--stats-json` (`npm run build-storybook -- --stats-json`, emitting `preview-stats.json`). Without that file the flag safely no-ops and every story renders - so if skipping never seems to happen, this is why. ## How it decides what changed `--only-changed` walks your pull request's file changes against the module dependency graph and selects every story that imports a changed file, directly or transitively. Everything else is carried forward: its existing baseline is reused as this build's result, with no render and no diff.  ## What triggers a full render? - No `preview-stats.json` in the build - you did not pass `--stats-json`, so there is no graph to trace. - A change to a file many stories import - a global stylesheet, a design-token file, the Storybook `preview`. - A lockfile or dependency change, which can shift anything. - The first build on a branch, which has nothing to carry forward yet. In each of those cases re-rendering everything is the correct, safe answer, so it does. ## Does it work with Playwright? No. `--only-changed` is a Storybook feature - it needs the module dependency graph to trace which stories a change affects, and a Playwright archive has no such graph. Archive builds always render what your tests captured. To keep those fast and cheap, make the captures deterministic instead - see [Fix flaky visual tests](/docs/deterministic-captures). ## Is it safe? Could it skip a story that really changed? A carried-forward story is one whose inputs your change did not touch, so its pixels should not have moved. The guarantee is only as good as the change detection, though, so treat any surprise as a signal: if you ever see a carried-forward story that clearly should have rendered, or a diff you cannot attribute to your own change, [investigate the baseline](/docs/baselines) rather than accepting it. > Skipping is one cost lever; writing fewer, denser stories is the other. See [Reduce visual test snapshots and cost](/docs/economical-stories). --- # Automated visual review with an AI judge Not every visual change is a bug. UI Verify's AI judge tells an intended change from a real regression, so your review queue is only what actually needs a human. Source: https://uiverify.ai/docs/the-ai-judge A diff tool tells you *that* pixels changed. It cannot tell you *whether* the change was the point of the pull request or a mistake. On a busy repo that leaves you a review queue full of intended changes to click through one by one. Unlike a pure pixel-diff tool such as Chromatic or Percy, UI Verify runs an AI judge over every changed story to do that triage first. ## What the judge does For each changed story it looks at three things - the baseline, the candidate, and the diff - and produces a verdict: a likely regression, or a likely intended change, with a short reason. Regressions surface at the top of the queue; intended changes are pre-sorted so accepting them is a single action.  ## The judge advises, you decide The judge never changes a baseline on its own. It ranks and explains; promoting a candidate to a baseline is always an explicit action, taken by a human or by an agent you authorized. A green build never means the AI quietly accepted something for you. ## Reviewing from your coding agent Because the diffs and verdicts are exposed over MCP, your coding agent can pull them straight into the pull-request flow - list the changes, read the judge's reasoning, accept the intended ones - without you leaving the terminal. See [Triage with your agent](/docs/triage-with-your-agent). > Coming from another tool? See [how UI Verify compares to Chromatic](/docs/uiverify-vs-chromatic) - the AI judge and agent review are the difference. --- # UI Verify vs Chromatic A Chromatic alternative built for agent-written UI: the same screenshot diffing, plus an AI judge that triages each change and MCP review from your coding agent. Source: https://uiverify.ai/docs/uiverify-vs-chromatic Chromatic pioneered Storybook visual testing, and UI Verify does the same core job: render every state, diff it against a baseline, and gate the pull request on review. If you are looking for a Chromatic alternative, here is where the two differ, stated plainly. ## What is the same - Per-story screenshot capture in a real browser, diffed pixel by pixel against a per-branch baseline. - A check and a comment on every pull request, with a review queue to accept or reject each change. - Storybook as a first-class capture path - your existing stories are the tests. - Skip-unchanged builds so a small PR does not re-render your whole Storybook. ## What is different - **An AI judge triages every change.** UI Verify labels each diff a likely regression or a likely intended change with a reason, so your queue is the handful that need a human - not a wall of expected diffs to click through. See [the AI judge](/docs/the-ai-judge). - **Review from your coding agent over MCP.** The diffs and verdicts are exposed over the Model Context Protocol, so Claude Code or Cursor can read a build and accept the intended changes without you opening a dashboard. See [Triage with your agent](/docs/triage-with-your-agent). - **No Storybook required.** UI Verify also captures real pages driven by Playwright via archive-replay, so you can visually test a Next.js app or a live staging URL, not just a component library. See the [Playwright quickstart](/docs/quickstart-playwright). - **Built for agent-written UI.** The whole flow assumes an agent may have written the PR, and puts verification where the agent already works. ## What about cost? Both tools bill per rendered snapshot, so the lever is the same on either one: render fewer stories. Two patterns - dense gallery stories and data-driven states - cut a component's snapshot count up to 10x with the same coverage, and [`--only-changed`](/docs/skip-unchanged) skips the stories a PR does not touch. See [Reduce visual test snapshots and cost](/docs/economical-stories) and the write-up on [cutting a Chromatic bill 10x](/blog/cut-chromatic-bill-10x). > Ready to try it? The [Storybook quickstart](/docs/quickstart-storybook) gets a diff check on your PRs in a few minutes, no migration required. --- # Reduce visual test snapshots and cost Every visual testing tool bills per story. Two patterns cut a component's snapshot count up to 10x with the same coverage, on UI Verify, Chromatic, or Percy. Source: https://uiverify.ai/docs/economical-stories So the story count is both your bill and your noise surface: more stories means more billable snapshots, and more places for a diff to flake. The naive one-story-per-variant-per-state-per-theme pattern explodes fast - a three-size, three-variant button in light and dark is already 18 snapshots. Two moves collapse it while keeping full coverage. ## Collapse a variant matrix into one gallery story A `Button` does not need separate Primary, Secondary, and Tertiary stories. Map through the prop combinations in one `AllVariants` story and render them in a grid. One screenshot then covers the whole matrix, and you get a single glanceable view of every permutation. ```tsx // ONE story covers every size x variant export const AllVariants = () => (