Skip to main content
Back to Blog
Guide
2026-06-09

Chromatic Visual Testing + Storybook TurboSnap Official 2026

Chromatic + Storybook TurboSnap guide. Cover the chromatic CLI, project-token, --only-changed, baselines, review workflow, and CI integration patterns.

Chromatic Visual Testing + Storybook TurboSnap Official 2026

Chromatic is the visual regression and review service built by the Storybook team. If you maintain a component library in Storybook, Chromatic plus TurboSnap gives you visual diffing, interaction testing, accessibility checks, and a review workflow that integrates natively with your component-driven development cycle. TurboSnap is the secret sauce: it analyzes your dependency graph and snapshots only the stories affected by your changes, cutting snapshot counts (and Chromatic costs) by 70-90% on most projects. This guide covers the full Chromatic stack as of 2026 -- installation, project token setup, the chromatic CLI flags, TurboSnap configuration, baselines, the review workflow, branch handling, and the GitHub Actions pattern most Storybook projects use.

If you are searching for chromatic visual testing storybook official docs, this guide walks you through every step from zero to gated PRs.

Key Takeaways

  • Chromatic publishes your Storybook to a cloud URL on every CI run, snapshots every story across the configured browser/viewport matrix, and diffs against the baseline
  • --only-changed activates TurboSnap, which uses your bundler dependency graph to skip unchanged stories. Typical speedup: 5-10x on warm runs
  • Authentication is via a per-project CHROMATIC_PROJECT_TOKEN. Tokens are write-only and safe to expose in CI but should not be committed to public source
  • The Chromatic CLI is one binary: npx chromatic. Common flags are --only-changed, --exit-zero-on-changes, --auto-accept-changes, --externals
  • Baselines are managed in the Chromatic UI. The first run becomes the baseline; subsequent diffs require approval to update it

Why Storybook + Chromatic

Storybook isolates each component in a controlled state. Chromatic captures that controlled state visually. The combination catches:

  • CSS regressions per component
  • Prop-driven visual variants (loading, error, empty states)
  • Accessibility regressions via the built-in a11y addon
  • Behavioral regressions via interaction tests in stories
  • Cross-browser visual differences

Chromatic is component-first. If you ship a design system or a heavily-componentized app, it is the strongest visual testing fit. For end-to-end visual testing of pages, see our Percy + Playwright guide.

Installing Chromatic

npm install --save-dev chromatic

Chromatic also requires Storybook 7 or 8. Storybook setup is out of scope for this guide; assume you already have stories at src/**/*.stories.tsx.

Getting a Project Token

Sign in to chromatic.com, create a project, copy the project token from the settings page. Tokens look like chpt_abc123.... Set in CI:

env:
  CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Locally:

export CHROMATIC_PROJECT_TOKEN=chpt_abc123...

First Publish

npx chromatic

What happens:

  1. Chromatic builds Storybook (build-storybook) into a static directory
  2. Uploads the bundle to Chromatic's cloud
  3. Renders every story across the configured browser/viewport matrix
  4. The first run establishes baselines; all stories show "New" in the UI
  5. Prints a build URL to the console

Open the build URL and approve the baseline. Subsequent runs diff against it.

TurboSnap

TurboSnap is Chromatic's killer feature. Activate with the --only-changed flag:

npx chromatic --only-changed

What it does:

  1. Reads your bundler dependency graph (Webpack, Vite)
  2. Compares the current commit to the base commit (default: main)
  3. Determines which source files changed
  4. Walks the import graph to find affected stories
  5. Snapshots only those stories; skips the rest

On a 500-story library where a single PR touches 3 components, TurboSnap might snapshot 20 stories instead of 500 -- a 25x reduction. Cost savings scale linearly because Chromatic charges per snapshot.

.chromatic Configuration

{
  "projectId": "Project:abc123",
  "onlyChanged": true,
  "externals": ["public/**", "src/assets/**"],
  "junitReport": true,
  "exitZeroOnChanges": false,
  "buildScriptName": "build-storybook"
}
KeyPurpose
onlyChangedEnable TurboSnap
externalsGlob patterns of files that affect every story (CSS variables, fonts, public assets)
exitZeroOnChangesExit 0 even when diffs exist (useful for non-blocking visual checks)
autoAcceptChangesGlob of branches where diffs auto-accept (main, release/*)
buildScriptNameNPM script to build Storybook
storybookBuildDirUse a pre-built Storybook directory

Chromatic CLI Flags Reference

FlagPurpose
--only-changedTurboSnap
--exit-zero-on-changesDon't fail CI on diffs
--auto-accept-changesAuto-approve all diffs on the branch
--externals "<glob>"Files that invalidate TurboSnap
--skipSkip the build entirely (useful for documentation-only branches)
--ciOptimize for CI environments
--branch-name <name>Override branch detection
--storybook-build-dir <path>Use prebuilt Storybook
--build-script-name <name>Override the npm script
--debugVerbose logging

Typical CI invocation:

npx chromatic --only-changed --exit-zero-on-changes

--exit-zero-on-changes lets you decide in GitHub branch protection whether to block on diffs.

Branch Strategy

Chromatic tracks baselines per branch and per branch hierarchy. The most common shape:

BranchBaseline sourceAuto-accept?
mainItselfYes
release/*Last releaseYes
feat/*mainNo (review required)
fix/*mainNo (review required)

Configure auto-accept via:

npx chromatic --auto-accept-changes=main,release/*

Or in the .chromatic config:

{
  "autoAcceptChanges": "main"
}

GitHub Actions Workflow

name: Chromatic
on:
  push:
    branches: [main]
  pull_request:

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - name: Run Chromatic
        uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          onlyChanged: true
          exitZeroOnChanges: true

The official chromaui/action wraps the CLI with sensible defaults. fetch-depth: 0 is critical -- TurboSnap needs the full git history to compute the dependency delta.

Review Workflow

When a PR creates diffs:

  1. Chromatic posts a check on the PR
  2. The PR author opens the Chromatic build URL
  3. Each diff shows side-by-side baseline vs current with a pixel overlay
  4. Reviewer accepts or denies each diff
  5. Accepted diffs become the new baseline when the PR merges

Required reviewers can be configured per-project. The Chromatic UI supports threaded comments on diffs, making it a true visual code review.

Interaction Tests

Storybook 7+ supports interaction testing via the play function. Chromatic captures the post-interaction state:

import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import { LoginForm } from './LoginForm';

const meta: Meta<typeof LoginForm> = {
  title: 'Forms/Login',
  component: LoginForm,
};
export default meta;

export const Empty: StoryObj<typeof LoginForm> = {};

export const ValidationError: StoryObj<typeof LoginForm> = {
  play: async ({ canvasElement }) => {
    const c = within(canvasElement);
    await userEvent.click(c.getByRole('button', { name: /sign in/i }));
    await expect(c.getByText(/email is required/i)).toBeVisible();
  },
};

Chromatic snapshots both stories: the empty form and the post-submission error state. Diffs reveal regressions in either layout.

A11y Testing

Storybook's accessibility addon runs axe-core on each story. With the Chromatic addon enabled, accessibility violations show up alongside visual diffs:

npm install --save-dev @storybook/addon-a11y

.storybook/main.ts:

import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  addons: ['@storybook/addon-a11y'],
  framework: '@storybook/react-vite',
};
export default config;

Configure failure thresholds per-story:

export const Default: StoryObj<typeof Button> = {
  parameters: {
    a11y: { config: { rules: [{ id: 'color-contrast', enabled: true }] } },
  },
};

Comparison: Chromatic vs Other Visual Tools

ToolBest forTradeoff
ChromaticStorybook-driven design systemsStorybook-specific
PercyPage-level visual tests with any frameworkNo native Storybook integration
ApplitoolsEnterprise, AI-powered diffingExpensive
Playwright toHaveScreenshotFree local diffingNo cloud UI, baseline conflicts
Backstop.jsSelf-hosted, freeDIY infrastructure

Pick Chromatic if your design system lives in Storybook. Pick Percy if your visual coverage is page-level.

TurboSnap Caveats

CaveatWhy it mattersFix
External CSS files not in webpack graphStyle changes don't trigger snapshotsAdd to --externals
Public assetsLogo or font changes missedAdd to --externals
Shallow git cloneTurboSnap failsfetch-depth: 0 in checkout
First TurboSnap runNeeds baseline referenceRun without --only-changed once
Force pushesBaseline lookup confusedAvoid force-pushing PRs

When TurboSnap misses changes, the symptom is "I changed CSS and no diffs appeared." Add the CSS to externals or run without TurboSnap on that branch.

externals Patterns

Common externals worth declaring:

{
  "externals": [
    "public/**",
    "src/assets/**",
    "src/styles/**",
    "src/theme/**",
    ".storybook/preview.ts",
    ".storybook/preview-head.html"
  ]
}

Storybook preview files affect every story. If you change .storybook/preview.ts without listing it as external, TurboSnap might think no stories changed.

Modes (Per-Story Variant Snapshots)

Chromatic Modes let you snapshot the same story under different parameters -- viewports, themes, locales -- without writing separate stories:

// .storybook/preview.ts
import { withThemeByClassName } from '@storybook/addon-themes';

export default {
  decorators: [
    withThemeByClassName({
      themes: { light: 'theme-light', dark: 'theme-dark' },
      defaultTheme: 'light',
    }),
  ],
  parameters: {
    chromatic: {
      modes: {
        'light desktop': { theme: 'light', viewport: { width: 1280 } },
        'dark mobile': { theme: 'dark', viewport: { width: 375 } },
      },
    },
  },
};

Each mode renders separately in Chromatic and gets its own baseline.

Externals for Vite vs Webpack

Vite and Webpack handle the dependency graph differently. The Chromatic CLI auto-detects, but if TurboSnap is missing changes:

BundlerCommon fix
ViteAdd CSS modules to externals if they import via ?inline
WebpackEnsure stats.json is enabled in build
Next.jsUse chromatic --storybook-build-dir storybook-static

Multi-Story Snapshots Per Component

A well-organized story file covers every visual variant:

import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Atoms/Button',
  component: Button,
};
export default meta;

type Story = StoryObj<typeof Button>;

export const Primary: Story = { args: { variant: 'primary', children: 'Primary' } };
export const Secondary: Story = { args: { variant: 'secondary', children: 'Secondary' } };
export const Disabled: Story = { args: { variant: 'primary', disabled: true, children: 'Disabled' } };
export const Loading: Story = { args: { variant: 'primary', loading: true, children: 'Loading' } };
export const WithIcon: Story = { args: { variant: 'primary', icon: 'check', children: 'Done' } };
export const FullWidth: Story = { args: { variant: 'primary', fullWidth: true, children: 'Wide' } };

Six stories = six snapshots per browser per viewport. With Chromatic's default matrix (Chrome at 320, 768, 1280), that's 18 renders per Button change.

Snapshot Disabling Per Story

If a story has too much legitimate visual churn (canvas animations, random data), opt out:

export const RandomAnimation: Story = {
  parameters: { chromatic: { disable: true } },
};

Or pause animations universally:

// .storybook/preview.ts
export default {
  parameters: {
    chromatic: { pauseAnimationAtEnd: true, delay: 300 },
  },
};

delay waits N ms before snapshotting to let animations settle.

Comparison Table -- Chromatic Plans

PlanSnapshots/monthSeatsApprox price
Free5,000Unlimited$0
Starter35,000Unlimitedstarts ~$149/mo
Standard85,000Unlimitedstarts ~$349/mo
Pro200,000+Unlimitedenterprise

TurboSnap is included in all paid tiers. Pricing is per-snapshot, so TurboSnap is essentially free money: every snapshot you skip is one you don't pay for.

Cost Optimization

TacticSnapshot reduction
Enable TurboSnap70-90%
Reduce viewport countUp to 67% (3 -> 1)
Reduce browser countUp to 75% (4 -> 1)
Use Modes selectively50-80%
Skip stories with chromatic: { disable: true }Per-story
Run only on PR (not push)50% (push and PR -> PR only)

Frequently Asked Questions

Does Chromatic work with Vue, Angular, Svelte?

Yes. Chromatic supports every Storybook framework: React, Vue, Angular, Svelte, Web Components, HTML, Preact, Solid. The CLI flags are framework-agnostic.

Can I use Chromatic without Storybook?

No. Chromatic is Storybook-specific. For page-level visual testing without Storybook, see Percy, Applitools, or Playwright's built-in screenshot diffing.

What is the difference between Chromatic and chromaui/action?

chromatic (the npm package) is the CLI. chromaui/action is a GitHub Action wrapper around it. Both invoke the same underlying engine. Use the action in GitHub Actions; use the CLI elsewhere.

How do I baseline new stories?

Run Chromatic once on the branch where the stories were introduced. The first render becomes the baseline. Approve it in the UI to make it the default.

Do interaction tests run on every snapshot?

The play function runs during Storybook capture. Chromatic snapshots the state after play completes. If the play function fails, the story is marked failed in the build.

Can I use Chromatic to gate merges?

Yes. In GitHub branch protection, require the "Chromatic" check to pass. Combined with required reviewers in Chromatic UI, no visual change merges without explicit approval.

What happens to baselines when I delete a story?

Chromatic detects removed stories and asks you to confirm removal in the review UI. Approving removes the baseline. Denying keeps it (useful if the story is temporarily disabled).

Conclusion

Chromatic plus Storybook plus TurboSnap is the most efficient visual regression setup available to component-first teams in 2026. The first integration takes an afternoon: install, set the token, run npx chromatic once for baselines, wire up the GitHub Action with TurboSnap. From there it scales as your design system scales -- new components get stories, stories get snapshots, snapshots get reviewed.

For more visual testing options, see our Percy + Playwright guide, the visual testing skills directory for AI agent skills that scaffold story files automatically, and our visual testing tool comparison to evaluate Chromatic, Percy, and Playwright screenshot diffing side by side.