Chromatic Cheat Sheet
Overview
Chromatic is a cloud-based visual testing and review platform specifically designed for Storybook. It captures snapshots of every story in your Storybook across multiple browsers and viewports, then compares them against approved baselines to detect visual regressions. Chromatic provides a collaborative review workflow for UI changes, similar to code review but for visual changes.
Built by the maintainers of Storybook, Chromatic offers deep integration with the Storybook ecosystem. It supports interaction testing, accessibility checks, component documentation publishing, and integrates with GitHub, GitLab, Bitbucket, and CI/CD pipelines. Chromatic also publishes your Storybook online for team sharing and stakeholder review.
Installation
# Install Chromatic
npm install -D chromatic
# Or with yarn
yarn add -D chromatic
# Or with pnpm
pnpm add -D chromatic
# Verify
npx chromatic --version
Basic Usage
# Run Chromatic (first time - builds and publishes Storybook)
npx chromatic --project-token=YOUR_PROJECT_TOKEN
# Run with auto-accept (for initial baseline)
npx chromatic --project-token=YOUR_PROJECT_TOKEN --auto-accept-changes
# Run against a specific branch
npx chromatic --project-token=YOUR_PROJECT_TOKEN --branch-name=main
# Skip build if no changes
npx chromatic --project-token=YOUR_PROJECT_TOKEN --exit-zero-on-changes
# Use environment variable for token
export CHROMATIC_PROJECT_TOKEN=YOUR_PROJECT_TOKEN
npx chromatic
CLI Options
| Flag | Description |
|---|---|
--project-token | Project token for authentication |
--auto-accept-changes | Auto-accept all visual changes |
--exit-zero-on-changes | Exit 0 even if there are changes |
--exit-once-uploaded | Exit after upload (don’t wait for results) |
--skip | Skip Chromatic entirely |
--only-changed | Only test stories that changed |
--only-story-names | Filter stories by name pattern |
--only-story-files | Filter by story file paths |
--externals | Glob patterns for external files that affect stories |
--storybook-build-dir | Use pre-built Storybook directory |
--build-script-name | npm script to build Storybook |
--branch-name | Override detected branch name |
--patch-build | Compare against specific base build |
--no-interactive | Disable interactive mode |
--debug | Enable debug output |
--dry-run | Run without uploading |
# Only test changed stories (TurboSnap)
npx chromatic --only-changed
# Specify external dependencies
npx chromatic --externals="public/**" --externals="src/styles/**"
# Use pre-built Storybook
npm run build-storybook
npx chromatic --storybook-build-dir=storybook-static
# Filter specific stories
npx chromatic --only-story-names="Button*"
npx chromatic --only-story-files="src/components/Button/**"
Storybook Configuration
Story-Level Configuration
// Button.stories.js
export default {
title: "Components/Button",
component: Button,
parameters: {
chromatic: {
// Capture at multiple viewports
viewports: [320, 768, 1200],
// Delay before capture (ms)
delay: 300,
// Diff threshold (0-1)
diffThreshold: 0.063,
// Disable for this story
disableSnapshot: false,
// Pause animations
pauseAnimationAtEnd: true,
},
},
};
export const Primary = {
args: {
primary: true,
label: "Button",
},
};
export const Loading = {
args: {
loading: true,
},
parameters: {
chromatic: {
// Extra delay for loading states
delay: 1000,
},
},
};
// Disable snapshot for specific story
export const Interactive = {
parameters: {
chromatic: { disableSnapshot: true },
},
};
Global Configuration
// .storybook/preview.js
export const parameters = {
chromatic: {
// Default viewports for all stories
viewports: [375, 768, 1280],
// Default delay
delay: 200,
// Diff threshold
diffThreshold: 0.063,
// Pause animations
pauseAnimationAtEnd: true,
},
};
Modes (Theme/Locale Testing)
// .storybook/modes.js
export const allModes = {
light: {
theme: "light",
},
dark: {
theme: "dark",
},
};
// .storybook/preview.js
import { allModes } from "./modes";
export const parameters = {
chromatic: {
modes: allModes,
},
};
Interaction Testing
import { within, userEvent } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
export const FilledForm = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Interact with the component
await userEvent.type(canvas.getByLabelText("Email"), "test@example.com");
await userEvent.type(canvas.getByLabelText("Password"), "password123");
await userEvent.click(canvas.getByRole("button", { name: "Submit" }));
// Chromatic captures snapshot AFTER interactions complete
await expect(canvas.getByText("Success")).toBeInTheDocument();
},
};
CI/CD Integration
GitHub Actions
name: Chromatic
on: push
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
- uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: true # TurboSnap
exitZeroOnChanges: true
autoAcceptChanges: "main" # Auto-accept on main
GitLab CI
chromatic:
image: node:20
script:
- npm ci
- npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN --exit-zero-on-changes
variables:
CHROMATIC_PROJECT_TOKEN: $CHROMATIC_TOKEN
Generic CI
#!/bin/bash
npm ci
npx chromatic \
--project-token="$CHROMATIC_PROJECT_TOKEN" \
--only-changed \
--exit-zero-on-changes \
--auto-accept-changes="main"
TurboSnap (Smart Snapshots)
# Only snapshot stories affected by code changes
npx chromatic --only-changed
# Specify external dependencies that affect stories
npx chromatic --only-changed \
--externals="src/styles/**" \
--externals="public/fonts/**" \
--externals=".storybook/**"
// Mark a story as always captured (even with TurboSnap)
export const CriticalComponent = {
parameters: {
chromatic: {
disableTurboSnap: true,
},
},
};
Advanced Usage
Custom Viewports
export default {
title: "Pages/Dashboard",
parameters: {
chromatic: {
viewports: [320, 768, 1024, 1440, 1920],
},
},
};
Handling Dynamic Content
export const WithDynamicData = {
parameters: {
chromatic: {
// Wait for content to load
delay: 2000,
// Increase diff threshold for minor rendering differences
diffThreshold: 0.1,
},
},
};
// Use decorators to mock dates/random data
export default {
decorators: [
(Story) => {
// Mock Date for consistent snapshots
jest.useFakeTimers().setSystemTime(new Date("2024-01-15"));
return <Story />;
},
],
};
Ignoring Elements
/* Add to elements that should be ignored in diffs */
[data-chromatic="ignore"] {
/* Chromatic will ignore changes in these elements */
}
export const WithAd = {
decorators: [
(Story) => (
<div>
<div data-chromatic="ignore">
<AdBanner />
</div>
<Story />
</div>
),
],
};
Configuration
# Environment variables
export CHROMATIC_PROJECT_TOKEN="chpt_xxxx"
export CHROMATIC_BRANCH="feature-branch"
export CHROMATIC_SHA="abc123"
# package.json script
{
"scripts": {
"chromatic": "chromatic --exit-zero-on-changes",
"chromatic:ci": "chromatic --auto-accept-changes=main --only-changed"
}
}
# chromatic.config.json
{
"projectToken": "chpt_xxxx",
"onlyChanged": true,
"externals": ["src/styles/**"],
"autoAcceptChanges": "main"
}
Troubleshooting
| Issue | Solution |
|---|---|
| Build fails to upload | Check project token; verify Storybook builds successfully |
| Too many changes detected | Use TurboSnap (--only-changed); check for flaky animations |
| Animations causing diffs | Set pauseAnimationAtEnd: true; add CSS to disable animations |
| Dynamic content causing diffs | Use delay parameter; mock dates and random data |
| Fonts not loading | Increase delay; add font CDN to --externals |
| TurboSnap not detecting changes | Ensure fetch-depth: 0 in git checkout; check --externals |
| Slow builds | Use --only-changed; pre-build Storybook with --storybook-build-dir |
| Baseline mismatch | Accept changes on main branch; use --auto-accept-changes |