Chromatic + Storybook: Visual Testing for UI Components

Chromatic + Storybook: Visual Testing for UI Components

Chromatic is a visual testing tool built specifically for Storybook. While Percy and Applitools operate at the page level (snapshot a URL), Chromatic operates at the component level — it captures every story in your Storybook and compares them against approved baselines.

This makes Chromatic the right choice for design system teams and component libraries. If your UI is built in React, Vue, Angular, or Svelte with Storybook, Chromatic slots in without changing your existing test architecture.

How Chromatic Works

  1. You push a commit
  2. GitHub Actions runs chromatic --project-token=...
  3. Chromatic publishes your Storybook to their cloud
  4. Chromatic renders every story across multiple browsers
  5. Changed stories are flagged for review in the Chromatic UI
  6. Your team approves or rejects the visual changes
  7. The GitHub status check passes or fails based on review outcome

The critical difference from page-level tools: Chromatic captures each story as an isolated component test, not a full application page. You test the Button component in all its variants, not the checkout page that happens to contain a button.

Setup

Install Chromatic

npm install --save-dev chromatic

Add a project

Sign up at chromatic.com, create a project, and get your project token. The token is not a secret — it identifies your project but doesn't grant write access.

Run Chromatic for the first time

npx chromatic --project-token=YOUR_TOKEN

The first run creates baselines for all stories. Subsequent runs compare against these baselines.

Writing Stories for Visual Testing

Chromatic tests every story you write. Good stories are the foundation of useful visual tests.

Component with variants

// src/components/Button/Button.stories.jsx
import { Button } from './Button';

export default {
  title: 'Components/Button',
  component: Button,
  parameters: {
    chromatic: { viewports: [375, 1280] }
  }
};

// Each named export is one story
export const Primary = {
  args: {
    variant: 'primary',
    children: 'Click me',
    size: 'medium'
  }
};

export const Secondary = {
  args: {
    variant: 'secondary',
    children: 'Cancel',
    size: 'medium'
  }
};

export const Destructive = {
  args: {
    variant: 'destructive',
    children: 'Delete account',
    size: 'medium'
  }
};

export const Loading = {
  args: {
    variant: 'primary',
    children: 'Saving...',
    loading: true,
    disabled: true
  }
};

export const Sizes = {
  render: () => (
    <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
      <Button size="small">Small</Button>
      <Button size="medium">Medium</Button>
      <Button size="large">Large</Button>
    </div>
  )
};

Stories that capture interaction states

Use Storybook's play function to interact with components before the visual snapshot is taken:

import { userEvent, within } from '@storybook/testing-library';

export const WithValidationErrors = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // Fill in invalid data
    await userEvent.type(
      canvas.getByLabelText('Email'),
      'not-an-email'
    );

    // Submit to trigger validation
    await userEvent.click(canvas.getByRole('button', { name: 'Submit' }));
  }
};

export const Focused = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    canvas.getByRole('button').focus();
  }
};

export const MenuOpen = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.click(canvas.getByRole('button', { name: 'Open menu' }));
    await canvas.findByRole('menu'); // wait for menu to appear
  }
};

Chromatic waits for the play function to complete before capturing the snapshot. This lets you test hover states, open menus, validation errors, and other interaction-triggered states.

Controlling Chromatic per-story

export const AnimatedComponent = {
  args: { /* ... */ },
  parameters: {
    chromatic: {
      // Pause animations at a specific point
      pauseAnimationAtEnd: true,
      // Override viewport for this story only
      viewports: [320, 1440],
      // Delay snapshot by 300ms (for animations)
      delay: 300,
      // Skip this story in Chromatic
      disableSnapshot: true
    }
  }
};

Disabling snapshots for documentation-only stories

Some stories are just documentation, not testable states:

export const Usage = {
  render: () => <div>This story shows usage examples</div>,
  parameters: {
    chromatic: { disableSnapshot: true }
  }
};

TurboSnap: Only Test Changed Components

TurboSnap is Chromatic's smart snapshot system. Instead of re-snapping every story on every commit, it analyzes your git diff and only snapshots stories whose dependencies changed.

npx chromatic --project-token=YOUR_TOKEN --only-changed

For a project with 500 stories, this can reduce snapshot count from 500 to 15-20 on a typical feature branch — dramatically reducing build time and cost.

TurboSnap is reliable for component changes but falls back to full snapshots when:

  • Configuration files change (chromatic.config.js, .storybook/main.js)
  • Global decorators change
  • Chromatic detects it can't confidently trace the dependency tree

Interaction Tests with @storybook/test

Combine visual tests with functional assertions in the same story:

import { expect } from '@storybook/test';
import { userEvent, within } from '@storybook/testing-library';

export const LoginFormSuccess = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Fill in credentials', async () => {
      await userEvent.type(canvas.getByLabelText('Email'), 'alice@example.com');
      await userEvent.type(canvas.getByLabelText('Password'), 'secret123');
    });

    await step('Submit form', async () => {
      await userEvent.click(canvas.getByRole('button', { name: 'Log in' }));
    });

    await step('Verify success state', async () => {
      await expect(canvas.getByText('Welcome back, Alice!')).toBeVisible();
    });
  }
};

Chromatic captures the final state after all interactions. The step labels appear in the Storybook interactions panel, making it easy to trace what happened.

The Review Workflow

When Chromatic detects visual changes, it creates a review build:

  1. Storybook published — your Storybook is accessible at chromatic.com/builds/...
  2. Changed stories listed — every story with a diff is flagged
  3. Side-by-side comparison — baseline vs current, with diff overlay
  4. Team review — anyone with project access can approve or deny
  5. CI status — the GitHub check passes when all changes are reviewed

Accepting changes

In the Chromatic review UI:

  • Click the checkmark to accept a change (it becomes the new baseline)
  • Click X to deny (the CI check fails)
  • Use "Accept all" for bulk acceptance when you've made an intentional design update
  • Leave comments on specific changes to document why something was approved

Squash merges and baselines

If your team uses squash merges, configure Chromatic to track main as the baseline branch:

npx chromatic --project-token=YOUR_TOKEN --auto-accept-changes main

This automatically accepts visual changes on the main branch (since they've already been reviewed in PR).

GitHub Actions Integration

name: Chromatic

on:
  push:
    branches-ignore:
      - main    # main uses auto-accept, not PR review

jobs:
  chromatic:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # TurboSnap needs full git history

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        run: npm ci

      - name: Run Chromatic
        uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          onlyChanged: true         # enable TurboSnap
          exitZeroOnChanges: false  # fail PR on unreviewed changes

For main branch (post-merge):

- name: Run Chromatic (main)
  uses: chromaui/action@latest
  with:
    projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
    autoAcceptChanges: true  # auto-accept on main after PR review

Storybook Composition

If you have multiple component libraries (a design system + application components), use Storybook Composition to test them all through one Chromatic project:

// .storybook/main.js
module.exports = {
  refs: {
    'design-system': {
      title: 'Design System',
      url: 'https://your-design-system.chromatic.com'
    }
  }
};

What Chromatic Catches

Chromatic catches:

  • Component visual regressions from CSS changes
  • Layout shifts when a dependency changes
  • Unexpected side effects from global CSS changes (e.g. changing body font-size breaks every component)
  • Cross-browser rendering differences
  • Responsive layout changes across viewports

Chromatic doesn't catch:

  • Application-level regressions (full page, multi-component flows)
  • Functional bugs in business logic
  • API integration issues
  • Performance regressions

For application-level visual testing, combine Chromatic (component level) with Percy or Applitools (page level).

Chromatic vs Percy vs Applitools

Tool Best for Snapshot level
Chromatic Storybook component libraries Component/story
Percy Multi-page web apps Page
Applitools Enterprise, cross-browser, AI diff Component and page

Most teams with a Storybook use Chromatic. Teams without Storybook use Percy or Applitools for page-level visual testing.

Getting Started Checklist

  • Write stories for every component state (default, hover, error, loading, empty)
  • Add play functions for interaction-triggered states (open menus, validation errors)
  • Configure viewports in parameters.chromatic for responsive components
  • Set disableSnapshot: true for documentation-only stories
  • Add Chromatic to your CI pipeline with --only-changed (TurboSnap)
  • Define your review workflow: who approves, when to auto-accept
  • Set up Slack notifications for new review builds

Chromatic is the most efficient visual testing tool for component library teams. If you use Storybook and you're not using Chromatic, you're leaving a significant testing layer uncovered.

Read more

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Atlantis automates Terraform plan and apply through pull requests. But Atlantis itself needs testing: workflow configuration, plan output validation, policy enforcement, and server health checks. This guide covers testing Atlantis workflows locally with atlantis-local, validating plan outputs with custom scripts, enforcing Terraform policies with OPA and Conftest, and monitoring Atlantis

By HelpMeTest