Build This Now
Build This Now
What Is Claude CodeInstallationNative InstallerFirst Project
speedy_devvkoen_salo
Blog/Handbook/Workflow/Visual Regression Tests

Visual Regression Tests

Build visual regression tests with Claude Code, Next.js 16, and Playwright. Create stable screenshot baselines, review diffs, and block accidental UI changes in CI.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →
speedy_devvWritten by speedy_devvPublished Jul 27, 202611 min readHandbook hubWorkflow index

Visual regression tests compare a rendered page with an approved screenshot and fail when pixels change beyond a defined threshold. In a Next.js 16 project, Claude Code can build and maintain that loop, while Playwright supplies the browser, stable screenshot capture, comparison engine, and failure artifacts.

This is most useful for changes that remain valid to the DOM but damage the interface: a clipped price, a shifted card, an invisible focus ring, or a mobile layout that wraps one line too early. The goal is not to reject every pixel difference. It is to turn meaningful visual changes into a reviewable engineering decision.

What visual regression testing covers

A visual test is one layer of a healthy test suite. It does not replace component assertions or user-flow tests, because a screenshot can look correct while a button is impossible to activate.

Test typeBest at detectingTypical blind spot
Unit or component testLogic, props, states, and accessibility rolesCross-component layout
End-to-end testNavigation, forms, and real user flowsSubtle visual damage
Screenshot comparisonSpacing, typography, color, clipping, and responsive layoutBehavior hidden behind a correct image

Playwright's toHaveScreenshot assertion captures the page repeatedly until two consecutive screenshots match, then compares the stable result with the stored baseline. That built-in stabilization helps, but it cannot make a live clock, random avatar, changing advertisement, or late-loading third-party widget deterministic. Those elements still need to be controlled or masked.

If the project has no browser coverage yet, read the Claude Code browser automation guide before expanding this setup. Visual checks work best after the core navigation path is already testable.

Build a deterministic test page

Start with one route and one desktop viewport. A narrow first target makes failures understandable and establishes a baseline-review habit before the suite expands.

The example below creates a small pricing preview with one intentionally dynamic value. Add it as app/visual-test/page.tsx; the test will mask the changing timestamp but verify the rest of the page.

import styles from "./page.module.css";

const features = [
  "Unlimited private projects",
  "Preview deployments",
  "Visual review history",
];

export default function VisualTestPage() {
  return (
    <main className={styles.shell}>
      <section className={styles.hero}>
        <p className={styles.eyebrow}>Developer workspace</p>
        <h1>Ship interface changes with confidence.</h1>
        <p className={styles.intro}>
          Review the rendered result before a visual mistake reaches production.
        </p>

        <article className={styles.card}>
          <div>
            <p className={styles.plan}>Team</p>
            <p className={styles.price}>
              $24 <span>per member</span>
            </p>
          </div>
          <ul>
            {features.map((feature) => (
              <li key={feature}>{feature}</li>
            ))}
          </ul>
          <button type="button">Start a workspace</button>
        </article>

        <p className={styles.build}>
          Preview generated at{" "}
          <span data-testid="build-time">{new Date().toISOString()}</span>
        </p>
      </section>
    </main>
  );
}

The CSS creates enough spacing, color, typography, and responsive behavior to make a visual check useful. Save it as app/visual-test/page.module.css; CSS Modules keep the fixture independent of whether the application uses Tailwind.

.shell {
  min-height: 100vh;
  padding: 72px 24px;
  color: #172033;
  background:
    radial-gradient(circle at 50% 0%, #e0e7ff 0, transparent 38%),
    #f8fafc;
}

.hero {
  width: min(100%, 720px);
  margin: 0 auto;
  text-align: center;
}

.eyebrow {
  margin: 0 0 12px;
  color: #4f46e5;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hero h1 {
  max-width: 650px;
  margin: 0 auto;
  font-size: clamp(40px, 7vw, 68px);
  line-height: 0.98;
  letter-spacing: -0.045em;
}

.intro {
  max-width: 540px;
  margin: 24px auto 40px;
  color: #526078;
  font-size: 19px;
  line-height: 1.6;
}

.card {
  display: grid;
  gap: 28px;
  width: min(100%, 460px);
  margin: 0 auto;
  padding: 32px;
  text-align: left;
  background: white;
  border: 1px solid #dbe2ee;
  border-radius: 24px;
  box-shadow: 0 24px 70px rgb(43 55 83 / 12%);
}

.plan {
  margin: 0 0 8px;
  font-weight: 700;
}

.price {
  margin: 0;
  font-size: 42px;
  font-weight: 800;
  letter-spacing: -0.04em;
}

.price span {
  color: #64748b;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0;
}

.card ul {
  display: grid;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.card li::before {
  margin-right: 10px;
  color: #4f46e5;
  content: "✓";
}

.card button {
  min-height: 48px;
  color: white;
  font: inherit;
  font-weight: 700;
  background: #312e81;
  border: 0;
  border-radius: 12px;
}

.build {
  margin-top: 22px;
  color: #64748b;
  font-size: 12px;
}

@media (max-width: 520px) {
  .shell {
    padding: 48px 16px;
  }

  .card {
    padding: 24px;
  }
}

The timestamp is realistic dynamic content, but it is not the subject of the test. Masking only that value keeps the surrounding sentence, typography, and position under visual coverage.

Install and configure Playwright

Install the Playwright test runner and the Chromium browser it controls. The browser installation is separate because Playwright pins compatible browser binaries for reliable local and CI execution.

Run these commands from the Next.js project root. The final command adds reusable scripts without asking you to edit package.json by hand.

npm install --save-dev @playwright/test
npx playwright install chromium
npm pkg set scripts.test:visual="playwright test"
npm pkg set scripts.test:visual:update="playwright test --update-snapshots"

A visual suite should control the environment more tightly than a general end-to-end suite. The configuration below fixes the browser, viewport, color scheme, locale, and time zone, disables animations for screenshot assertions, and keeps CI workers at one to reduce machine-level variance.

Save this complete configuration as playwright.config.ts. The webServer option starts Next.js before the tests and reuses an existing local server outside CI.

import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests/visual",
  fullyParallel: false,
  forbidOnly: Boolean(process.env.CI),
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: process.env.CI
    ? [["line"], ["html", { open: "never" }]]
    : [["list"], ["html", { open: "never" }]],
  expect: {
    toHaveScreenshot: {
      animations: "disabled",
      caret: "hide",
      scale: "css",
      maxDiffPixelRatio: 0.005,
      pathTemplate:
        "{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}",
    },
  },
  use: {
    baseURL: "http://127.0.0.1:3000",
    colorScheme: "light",
    locale: "en-US",
    timezoneId: "UTC",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
  projects: [
    {
      name: "chromium-desktop",
      use: {
        ...devices["Desktop Chrome"],
        viewport: { width: 1440, height: 1000 },
      },
    },
  ],
  webServer: {
    command: "npm run dev",
    url: "http://127.0.0.1:3000",
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

The ratio allows a very small amount of pixel variation without accepting a visibly broken layout. Tune it from observed, reviewed failures rather than choosing a large tolerance up front. Playwright documents the complete screenshot options in its toHaveScreenshot API.

Write the first screenshot test

Wait for web fonts before capturing the page. A screenshot taken while the fallback font is visible can shift every line even though the application code is correct.

Create tests/visual/visual-test.spec.ts with the following test. The assertion verifies the complete scrollable page and replaces the changing timestamp with Playwright's standard mask color.

import { expect, test } from "@playwright/test";

test("pricing preview matches its approved design", async ({ page }) => {
  await page.goto("/visual-test");
  await page.evaluate(() => document.fonts.ready);

  await expect(page).toHaveScreenshot("pricing-preview.png", {
    fullPage: true,
    mask: [page.getByTestId("build-time")],
  });
});

Generate the first approved image locally, inspect it, and commit it with the test. The update command is an approval operation, not a generic repair command.

npm run test:visual:update
npm run test:visual
git status --short

After a later UI change, run the normal test command first. If it fails, open playwright-report/index.html with npx playwright show-report, compare the expected, actual, and diff views, then either fix the regression or deliberately regenerate the baseline.

Add visual tests to CI

CI should use the same package lock, browser family, and command as local development. Playwright's CI documentation recommends installing browser dependencies in the workflow, while the HTML report gives reviewers the evidence needed to diagnose a failure.

The following GitHub Actions workflow runs on pull requests and uploads the report only when the visual command fails. Save it as .github/workflows/visual-regression.yml.

name: Visual regression

on:
  pull_request:

jobs:
  screenshots:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Install Chromium
        run: npx playwright install --with-deps chromium

      - name: Compare screenshots
        run: npm run test:visual

      - name: Upload Playwright report
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

Linux and macOS can render fonts differently. Generate the committed baseline in the same Linux environment used by CI, or use Playwright's official container for both baseline generation and comparison. Do not compensate for operating-system drift with a broad threshold.

For a broader delivery pipeline, pair this job with the checks in the Claude Code GitHub Actions guide. Screenshot approval should remain visible in the pull request instead of becoming an invisible automation side effect.

Give Claude Code review rules

Claude Code is effective at adding page coverage because it can inspect the component, identify dynamic regions, run the test, and explain the diff. It still needs a firm rule against blindly replacing evidence.

Add a focused section to the repository's CLAUDE.md. These instructions make the expected behavior repeatable across sessions and keep snapshot updates under human review.

## Visual regression rules

- Run `npm run test:visual` after changing shared layout, typography, or pricing UI.
- Never run `npm run test:visual:update` only to make a failure disappear.
- Inspect the expected, actual, and diff images before proposing a baseline update.
- Fix unstable data at its source. Mask only content that must change between runs.
- Keep one intentional assertion per screenshot and use descriptive baseline names.
- Report whether each failure is a regression, an intended design change, or instability.

A useful Claude Code prompt names the route, viewport, dynamic regions, and acceptance rule. For example: “Add visual coverage for the account settings route at the existing desktop project. Mask only the signed-in user's avatar, run the test, and explain every diff before changing a baseline.”

This process fits naturally beside test-driven development with Claude Code. The behavioral test defines what the feature does; the screenshot defines the approved rendered state.

Grow the suite without making it noisy

Expand by user risk, not by page count. Checkout, authentication, pricing, navigation, and shared responsive shells usually deserve coverage before low-traffic informational pages.

Use a separate Playwright project when a second viewport represents a real design contract. A mobile project at a fixed device profile is clearer than changing viewport dimensions inside one test, and its baseline path remains separate through the project-name token.

Keep these boundaries as the suite grows:

  • Mock or seed API data so the same entities render every time.
  • Freeze clocks when dates affect layout, and mask only when the exact date is irrelevant.
  • Host fonts locally or ensure they finish loading before capture.
  • Disable animations through Playwright rather than adding test-only CSS throughout the product.
  • Review each updated PNG in the pull request.
  • Delete obsolete baselines when routes or projects disappear.

Avoid pixel snapshots for highly personalized dashboards, live maps, videos, and advertising surfaces unless those regions can be made deterministic. A role-based assertion may provide more signal for those areas.

The practical success measure is not the number of images. It is whether a failed comparison points to one understandable decision and gives the reviewer enough evidence to make it quickly.

Frequently asked questions

How do I run visual regression tests with Claude Code?

Give Claude Code one deterministic Playwright command, a small set of screenshot tests, and an explicit rule that baselines change only after a human reviews the diff. Generate and commit the approved baseline, then run the comparison in CI on every relevant pull request.

Should Playwright screenshots be committed to Git?

Yes. Approved baseline PNGs are test inputs and belong in Git beside the test that uses them. Failure artifacts such as the actual image and pixel diff belong in the CI artifact, where reviewers can inspect them without adding them to repository history.

How do I prevent flaky Playwright screenshot tests?

Use the same browser and operating system for baseline creation and CI. Fix the viewport, locale, time zone, color scheme, fonts, and test data; disable animations; wait for font loading; and mask only the smallest region that must change. A large pixel threshold usually hides instability instead of fixing it.

Code Kit is a $29 one-time harness that runs on top of Claude Code. It helps plan, build, evaluate, and test changes with quality gates for zero type errors, zero lint errors, and a clean production build; Claude Code itself requires a paid Anthropic plan.

Posted by @speedy_devv

Continue in Workflow

  • Agentic Commerce: How to Build an App AI Agents Can Pay For
    A plain-English guide to agentic commerce in 2026: what x402, ACP, and the Machine Payments Protocol do, plus a weekend walkthrough for shipping a paid API that AI agents can buy from.
  • Claude Code Best Practices
    Five habits separate engineers who ship with Claude Code: PRDs, modular CLAUDE.md rules, custom slash commands, /clear resets, and a system-evolution mindset.
  • Claude Code Auto Mode
    A second Sonnet model reviews every Claude Code tool call before it fires. What auto mode blocks, what it allows, and the allow rules it drops in your settings.
  • Channels, Routines, Teleport, Dispatch
    The four Claude Code features Anthropic shipped in March and April 2026 that turn the CLI into an event-driven coordination layer across phone, web, and desktop.
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Agent Fundamentals
    Five ways to build specialist agents in Claude Code: Task sub-agents, .claude/agents YAML, custom slash commands, CLAUDE.md personas, and perspective prompts.
  • Agent Harness Engineering
    The harness is every layer around your AI agent except the model itself. Learn the five control levers, the constraint paradox, and why harness design determines agent performance more than the model does.
  • Agent Patterns
    Orchestrator, fan-out, validation chain, specialist routing, progressive refinement, and watchdog. Six orchestration shapes to wire Claude Code sub-agents with.
  • Agent Teams Best Practices
    Battle-tested patterns for Claude Code Agent Teams. Context-rich spawn prompts, right-sized tasks, file ownership, delegate mode, and v2.1.33-v2.1.45 fixes.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →

On this page

What visual regression testing covers
Build a deterministic test page
Install and configure Playwright
Write the first screenshot test
Add visual tests to CI
Give Claude Code review rules
Grow the suite without making it noisy
Frequently asked questions
How do I run visual regression tests with Claude Code?
Should Playwright screenshots be committed to Git?
How do I prevent flaky Playwright screenshot tests?

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →