# 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. ![A finished UI Verify build showing screenshot counts, an agent-activity panel, and stories grouped into failed, changed, and new.](https://uiverify.ai/docs/build-overview-dark.png) ## 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. ![A passed UI Verify build showing 21 rendered and 103 carried forward, with no visual changes.](https://uiverify.ai/docs/clean-build-dark.png) ## 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. ![A UI Verify diff review: baseline and candidate side by side with accept, deny, and ignore controls and a change history below.](https://uiverify.ai/docs/ai-verdict-dark.png) ## 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 = () => (
{(['sm', 'md', 'lg'] as const).flatMap((size) => (['primary', 'secondary', 'danger'] as const).map((variant) => ( )), )}
); // 9 buttons, 1 snapshot instead of 9. ``` ## Drive component states from data Same idea for a component that loads data. Instead of a Loading, Empty, and Error story each, pass the response shape straight in and let one `AllStates` story map through the shapes. Deterministic input in, deterministic pixels out, many states, few shots. > Your coding agent can refactor an exploding story matrix into this shape for you - the [economical stories skill](/skills/economical-stories) does it. For the full write-up, see [How I cut my Chromatic bill 10x](/blog/cut-chromatic-bill-10x). ## Why fewer stories is also less flake Every story is a place a diff can flake. Collapsing thirty near-identical stories into three dense ones does not just shrink the bill - it shrinks the number of surfaces a false positive can appear on. Cheaper and quieter are the same move. On top of this, [`--only-changed`](/docs/skip-unchanged) skips even the dense stories a PR does not touch. --- # Fix flaky visual tests A visual test should only diff when the UI really changed. Every flaky diff traces to run-to-run variation you can eliminate at capture time. Source: https://uiverify.ai/docs/deterministic-captures A flaky diff comes back changed when nothing actually changed. The screenshot is not lying - it faithfully caught something that moves between runs, like the clock or an animation frame. Remove that source and the flake goes with it. ## Common causes of flaky visual tests - **The clock** - relative timestamps like "2 hours ago", or a date rendered from `Date.now()`. - **Animations** - an infinite CSS or JS animation caught mid-frame at a different point each run. - **Live data** - a component that fetches a real API renders different content every time. - **Randomness** - `Math.random`, generated ids, shuffled orders. - **Third-party widgets** - maps, embeds, ad slots that load their own moving content. - **Web fonts** - text captured before the font swaps in reflows on the next run. ## Fix them at record time - Freeze the clock to a fixed instant so relative-time labels are identical every run. - Disable or pause animations for the capture. - Feed components fixture data instead of a live fetch. - Seed or stub any randomness to a fixed value. - Stub third-party embeds behind a static placeholder. - Wait for fonts to load before the screenshot is taken. ## Storybook vs real pages For Storybook, control the variation inside the story - fixed props, frozen time, fixture data. For real pages driven by Playwright, archive-replay bakes the page in at record time, so a live API or a feature flag cannot shift it on replay - but you still freeze the clock and stub moving widgets before you record. > The full symptom-to-fix checklists live in the [Storybook determinism](/skills/storybook-visual-testing) and [Playwright determinism](/skills/playwright-visual-testing) skills, so your agent can apply them for you. --- # Triage visual changes from your coding agent Review a UI Verify build without leaving your terminal. The UI Verify MCP lets Claude Code, Cursor, or any MCP client read a build's changes and accept baselines. Source: https://uiverify.ai/docs/triage-with-your-agent If an agent wrote the pull request, an agent can review the visual changes. UI Verify exposes builds over the Model Context Protocol, so a coding agent - Claude Code, Cursor, or any MCP client - can pull a build's changes into the conversation, look at each diff, read the AI judge's reasoning, and accept the intended ones. ## What the agent can do - List a build's changed stories. - Fetch a diff image and the AI judge's verdict for any story. - Bucket changes into real regressions vs cosmetic reflow vs rendering noise. - Summarize the real regressions into a PR comment. - Accept baselines in bulk once you have signed off on the direction. ## The loop 1. The agent opens the pull request and the UI Verify check runs. 2. The agent lists the build's changes over MCP. 3. It reads each diff and the judge's verdict. 4. It accepts the intended changes, and flags anything it cannot attribute to the diff for you to look at. > The [triage visual changes skill](/skills/triage-visual-changes) packages this whole loop into one command your agent can run. ## Why this matters for agent-written UI The bottleneck on an agent's pull request is verification, not authorship. Putting the diff and a verdict where the agent already works closes the loop: the same place the code was written is where its pixels get checked, with no context switch to a dashboard in the middle. --- # Troubleshooting UI Verify Fixes for the common UI Verify setup problems: no check on your PR, an empty Playwright archive, phantom diffs after branching, and auth failures on upload. Source: https://uiverify.ai/docs/troubleshooting Most first-run problems fail quietly - the upload succeeds and something downstream is just missing. Here are the ones we see most, each with the reason and the fix. ## The upload succeeds but nothing shows up on my PR The GitHub App is not installed, or not pointed at this repo. Uploads work with only the API key, but the check and PR comment need the App. Install it from your project settings and select the repo - see step 4 of the [Storybook quickstart](/docs/quickstart-storybook). ## My Playwright build uploaded nothing Installing `@uiverify/playwright` is not enough - you have to swap the import in your tests to `import { test, expect } from "@uiverify/playwright"`. Without the swap, nothing is written to `./uiverify-archive` and the upload has an empty directory. See step 2 of the [Playwright quickstart](/docs/quickstart-playwright). ## Every PR shows changes I didn't make Your CI is checking out a shallow clone, so UI Verify cannot resolve the right baseline from your branch history. Add `fetch-depth: 0` to your `actions/checkout` step. If only one or two stories look off, the branch may just be behind - see [Baselines and branches](/docs/baselines). ## playwright test fails on CI with a missing browser A clean CI runner has no browsers installed. Add `npx playwright install --with-deps` before you run your tests, as in the [Playwright quickstart](/docs/quickstart-playwright) CI workflow. ## The upload fails with an authentication error The `UIVERIFY_API_KEY` is missing or wrong. Locally, pass it inline (`UIVERIFY_API_KEY=your_key npx -y uiverify@latest upload ...`) or export it first; in CI, add it as a repository secret and reference it under `env`. The key is per project - copy it from that project's settings. ## --only-changed renders everything anyway It needs Storybook's dependency graph, which only exists when you build with `--stats-json`. Without `preview-stats.json` the flag safely falls back to a full render. See [Skip unchanged stories](/docs/skip-unchanged).