Visual Regression Testing with Storybook and Chromatic

Visual Regression Testing with Storybook and Chromatic

Visual regression testing catches unintended UI changes by comparing screenshots of your components against known-good baselines. Storybook + Chromatic is the most widely adopted setup for this: Storybook provides isolated component renders across all states, and Chromatic handles screenshot capture, diffing, and the review workflow.

This guide covers everything from initial setup to running visual tests in CI and managing baselines efficiently.

How Storybook + Chromatic Works

The workflow has three parts:

  1. Stories as test cases — each story is a component state that gets screenshotted
  2. Chromatic captures — on every CI run, Chromatic renders each story in a cloud browser and takes a screenshot
  3. Diff review — if pixels changed, Chromatic flags it. A team member reviews and either accepts (new baseline) or rejects (regression)

This approach scales well because you don't write separate visual test scripts — your existing stories double as visual test cases.

Setup

Install Chromatic

npm install --save-dev chromatic

Get Your Project Token

Create an account at chromatic.com and connect your repository. Chromatic gives you a project token — keep it in your CI secrets as CHROMATIC_PROJECT_TOKEN.

Run Chromatic

npx chromatic --project-token=<your-token>

On first run, Chromatic builds your Storybook, renders every story, and saves baseline screenshots. On subsequent runs, it compares new screenshots to those baselines.

Writing Stories for Visual Testing

Every story is a visual test case. Think about what states matter visually:

// Button.stories.tsx
export const Primary: Story = { args: { variant: 'primary', label: 'Save' } };
export const Secondary: Story = { args: { variant: 'secondary', label: 'Cancel' } };
export const Disabled: Story = { args: { disabled: true, label: 'Save' } };
export const Loading: Story = { args: { loading: true, label: 'Saving...' } };
export const WithIcon: Story = { args: { icon: 'check', label: 'Done' } };

Each of these becomes a separate screenshot comparison. If Loading state suddenly renders differently, Chromatic catches it even if the other states look fine.

Controlling Viewport

Test at multiple breakpoints by setting viewport sizes in stories:

export const Mobile: Story = {
  parameters: {
    viewport: { defaultViewport: 'mobile1' },
    chromatic: { viewports: [375, 768, 1280] },
  },
};

The chromatic.viewports parameter tells Chromatic to capture the story at each listed width.

Disabling Visual Tests for Specific Stories

Some stories change on every render (timestamps, random data, animations). Exclude them from visual diffing:

export const AnimatedLoader: Story = {
  parameters: {
    chromatic: { disableSnapshot: true },
  },
};

Handling Animations

Animations cause false positives because screenshots capture a mid-animation frame. Disable animations in Chromatic:

// .storybook/preview.ts
export const parameters = {
  chromatic: {
    pauseAnimationAtEnd: true, // Wait for CSS animations to complete
  },
};

For JavaScript animations, pause them explicitly:

export const AnimatedCard: Story = {
  parameters: {
    chromatic: { delay: 300 }, // Wait 300ms before screenshot
  },
};

The Review Workflow

When Chromatic detects pixel changes, it creates a build with diffs for review.

What "Changed" Means

Chromatic highlights the exact pixels that changed in red. You see:

  • Before — baseline screenshot
  • After — new screenshot
  • Diff — pixels that changed, highlighted

Changes fall into two categories:

  1. Intentional — you changed a button color, updated a font. Accept the diff to make it the new baseline.
  2. Regression — an unintended side effect broke the visual appearance. Reject it.

Accepting Changes

Accept diffs to update the baseline. You can accept:

  • A single story's diff
  • All diffs in a build
  • All diffs matching a component

Once accepted, those screenshots become the new baseline for future comparisons.

PR Integration

Connect Chromatic to your GitHub/GitLab repo and it posts build status to PRs automatically:

  • ✅ No visual changes — PR can merge
  • ⚠️ Changes detected — review required before merge
  • ❌ Build failed — Storybook couldn't render

Configure it to block PR merges until visual review completes:

# GitHub: require the chromatic/ui-review check to pass

CI Integration

GitHub Actions

name: Visual Testing
on: [push, pull_request]

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required for TurboSnap
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: Publish to Chromatic
        uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          onlyChanged: true  # TurboSnap — only test affected stories

The fetch-depth: 0 is required for TurboSnap (see below).

GitLab CI

chromatic:
  image: node:20
  script:
    - npm ci
    - npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN
  only:
    - merge_requests
    - main

TurboSnap: Faster CI

TurboSnap analyzes your git changes and only takes snapshots of stories that could be affected by those changes. If you change Button.tsx, only Button.stories.tsx runs — not your entire Storybook.

Enable it with --only-changed:

npx chromatic --project-token=<token> --only-changed

On large Storybooks with hundreds of stories, TurboSnap reduces CI time by 50-80%.

Requirements for TurboSnap:

  • Full git history available (fetch-depth: 0 in GitHub Actions)
  • Storybook configured with staticDirs pointing to your actual assets
  • No dynamic imports that bypass dependency tracking

Baseline Management

Accepting on Main Branch

Baselines are tied to branches. When you accept a diff on a feature branch, it only updates the baseline for that branch. When the branch merges to main, the accepted baseline becomes the main branch baseline.

This means:

  • Feature branch diffs don't pollute main baselines
  • Only reviewed, accepted changes become permanent

Rerunning Builds

If you accidentally accept a regression, rerun the previous build to restore that baseline:

npx chromatic --project-token=<token> --rebuild-detector

Or use the Chromatic UI to rebaseline from a specific historical build.

Squash Commits and Rebases

If your repo squashes commits, git history gets rewritten. Tell Chromatic to use the merge commit:

npx chromatic --project-token=<token> --patch-build=<base-branch>

Storybook Modes (Matrix Testing)

Chromatic Modes let you test stories across multiple themes, locales, or color schemes without writing separate stories:

// .storybook/modes.ts
export const allModes = {
  'light desktop': {
    backgrounds: { value: '#ffffff' },
    viewport: { width: 1280 },
  },
  'dark desktop': {
    backgrounds: { value: '#1a1a1a' },
    viewport: { width: 1280 },
  },
  'light mobile': {
    backgrounds: { value: '#ffffff' },
    viewport: { width: 375 },
  },
};

Apply to individual stories or globally:

export const Card: Story = {
  parameters: {
    chromatic: {
      modes: {
        'light desktop': allModes['light desktop'],
        'dark desktop': allModes['dark desktop'],
      },
    },
  },
};

Chromatic captures one screenshot per mode — you get visual coverage for every combination without duplicating stories.

Cost Management

Chromatic pricing is based on snapshots (screenshots taken per build). To manage costs:

  1. Use TurboSnap — only test changed components
  2. Disable snapshots for dynamic storieschromatic: { disableSnapshot: true }
  3. Limit viewport testing — don't test all 3 viewports for every story; reserve multi-viewport for layout-sensitive components
  4. Skip stories on main — only run full snapshots on PRs; use --skip on merge commits if CI cost is a concern

Comparing with Self-Hosted Alternatives

Chromatic is a paid service. Self-hosted alternatives include:

  • Loki — free, stores baselines in your repo, runs locally
  • Percy — similar to Chromatic, BrowserStack product
  • BackstopJS — open source, more configuration required
  • reg-suit — open source, integrates with S3 for baseline storage

Chromatic's advantages are the GitHub integration, managed infrastructure, and the review workflow being purpose-built for visual testing. For teams that want to avoid SaaS costs, Loki with Storybook is a viable alternative — see the Loki guide for setup.

When Visual Regression Testing Pays Off

Visual regression testing is most valuable when:

  • You have a component library or design system used across many apps
  • Multiple developers touch UI code simultaneously
  • You ship frequently and can't manually verify every component after each deploy
  • Your components have many visual states (loading, error, empty, disabled, hover)

The upfront investment is low — your stories already exist. Adding Chromatic is a one-line CI change. The payoff is catching accidental regressions before users do.

Read more