Percy vs Chromatic for component visual testing

This page belongs to the Visual Testing Tool Comparisons section of the Visual Regression & Snapshot Strategies guide. Once you have decided you want a hosted visual testing service driven by Storybook — rather than self-hosted Playwright screenshots — the choice narrows to two mature products: Percy (BrowserStack) and Chromatic. This page compares them on the five axes that actually decide the outcome: Storybook integration depth, responsive-width capture, review workflow, baseline branching, and pricing model.

Problem statement

Percy and Chromatic look interchangeable in a feature-matrix skim. Both snapshot your Storybook in the cloud, both render a diff, both gate a pull request. Teams pick one on a coin flip, wire it into CI, and only discover the differences that matter three months in — when the monthly bill scales differently than expected, when responsive breakpoints double the snapshot count, or when a long-lived feature branch drowns reviewers in diffs against unrelated main changes.

The concrete pain: a design-system team adopts one tool, defines six responsive widths because their components are fluid, and finds their snapshot consumption is six times what they modeled — because the two tools bill responsive widths on completely different meters. The tool was fine; the mental model of how it charges was wrong.

Root cause

Percy and Chromatic differ most in what a snapshot is and what a baseline is tied to — and those two decisions ripple into responsive capture, review, and pricing.

Percy takes one DOM capture per story and re-renders that single capture at each configured width inside its cloud, then diffs each rendering. Chromatic captures a discrete snapshot per story per viewport per browser on its hosted browsers. That difference is why the same six-width component costs differently on each. On baselines, Chromatic — built by the Storybook team — binds each accepted baseline to a git branch, while Percy compares against a single configured base branch. Understanding those two roots explains every downstream trade, including the threshold and tolerance knobs each exposes for suppressing diff noise.

Percy re-render model vs Chromatic per-viewport snapshots Left lane: Percy takes one DOM capture of a story and re-renders it at three widths in its cloud, billed as one screenshot with re-renders. Right lane: Chromatic captures a separate snapshot for each viewport and browser combination, each billed independently. Percy one capture, re-rendered per width 1 DOM capture 375px 768px 1280px billed: 1 screenshot, 3 renders Chromatic discrete snapshot per viewport story parameters snap 1 snap 2 snap 3 billed: 3 snapshots

Minimal reproduction

The same fluid PriceCard story, captured at three widths, meters differently on each service:

// PriceCard.stories.jsx — one story, three responsive widths wanted
import { PriceCard } from '../src/PriceCard';

export default { component: PriceCard, title: 'PriceCard' };

export const Standard = {
  args: { plan: 'Team', price: 29 },
  parameters: {
    // Percy re-renders one capture at each width — 1 screenshot
    percy: { widths: [375, 768, 1280] },
    // Chromatic captures one snapshot per viewport — 3 snapshots
    chromatic: { viewports: [375, 768, 1280] },
  },
};

One story, three widths: Percy counts this as one screenshot with three renders; Chromatic counts it as three snapshots. Multiply across a 40-story library and the difference is what surprises the invoice.

Step-by-step fix

1. Configure Percy against Storybook

npm install --save-dev @percy/cli @percy/storybook
# .percy.yml
version: 2
snapshot:
  widths: [375, 768, 1280]        # applied to every story unless overridden
  min-height: 1024
  percy-css: |
    .animated-spinner { animation: none !important; }   # freeze motion for stable diffs
storybook:
  args:
    docs: false
# build Storybook, then snapshot every story through Percy
npm run build-storybook
npx percy storybook ./storybook-static --project-token=percy_tok_1a2b3c

What this does: percy storybook enumerates each story, uploads one DOM capture per story to Percy’s cloud, and re-renders it at each width in widths. The percy-css block neutralizes animation so a spinner mid-frame does not cause a false diff.

2. Configure Chromatic against Storybook

npm install --save-dev chromatic
// .storybook/preview.js — project-wide Chromatic defaults
export const parameters = {
  chromatic: {
    viewports: [375, 768, 1280],   // one snapshot per viewport
    pauseAnimationAtEnd: true,      // settle animations before capture
    diffThreshold: 0.063,           // tolerance, see the Chromatic threshold guide
  },
};
# build + upload Storybook; Chromatic renders each story on hosted browsers
npx chromatic --project-token=chpt_a1b2c3d4e5f6 --only-changed

What this does: Chromatic builds your Storybook, uploads it, and captures a discrete snapshot per story per viewport. --only-changed (TurboSnap) uses the git diff and webpack dependency graph to skip stories unaffected by the commit, which is Chromatic’s primary lever for controlling snapshot volume.

3. Compare on the five deciding axes

Axis Percy (BrowserStack) Chromatic
Storybook integration @percy/storybook plugin enumerates stories; per-story percy param Built by Storybook team; story params are snapshot config
Responsive-width capture One capture re-rendered per widths entry Discrete snapshot per chromatic.viewports entry
Cross-browser Chrome, Firefox, Safari, Edge (plan-dependent) Chrome, Firefox, Safari, Edge; each browser a snapshot
Review workflow Snapshot-approval board, group by diff Per-story accept/deny UI, comments, assignments
Baseline model Diff against configured base branch Baseline bound to git branch, carried on merge
Change-skip optimization Responsive re-render (fewer captures) TurboSnap skips unchanged stories via git graph
Pricing meter Per screenshot (widths re-rendered free-ish) Per snapshot (each viewport/browser counts)
Best-fit signal Many responsive widths, one main branch Deep Storybook use, many branches, per-story review

4. Recommend by scenario

  • Fluid components needing many responsive widths, team works off one main → Percy. The re-render-per-width model keeps cost flat as you add breakpoints, and single-base-branch baselines match your workflow.
  • A design system with heavy Storybook use and many concurrent feature branches → Chromatic. Git-branch baselines stop cross-branch diff noise, TurboSnap controls volume, and the per-story review UI fits designer sign-off. Gate it as a required visual review check.
  • You already pay for BrowserStack → Percy folds into that account and its real-device cross-browser grid, avoiding a second vendor.
  • You need per-viewport browser fidelity over cost → Chromatic, because each width is a true independent capture rather than a re-render of one DOM.

Verification

Run both against the same commit and read the reported snapshot counts — that is the number your bill is built on.

# Percy — reports screenshots, widths folded in
npx percy storybook ./storybook-static
# [percy] Snapshot taken: 'PriceCard: Standard'
# [percy] Finalized build #128: 40 snapshots, 3 widths each
# [percy] https://percy.io/org/design-system/builds/128

# Chromatic — reports snapshots per viewport
npx chromatic --project-token=chpt_a1b2c3d4e5f6
#   ✓ Started build 128
#   ✓ Published 40 stories → 120 snapshots (3 viewports)
#   → 2 changes must be reviewed
#   https://chromatic.com/build?appId=...&number=128

The same 40 stories at three widths read as “40 snapshots, 3 widths each” on Percy and “120 snapshots” on Chromatic. Both are correct; they are just different meters. Model your cost on the meter the tool actually bills, not on story count.

Edge cases and caveats

  • TurboSnap can under-capture on config changes. Chromatic’s --only-changed relies on the webpack dependency graph; a change to a global theme file or a .storybook/preview.js decorator may not be traced to every dependent story, so periodically run a full build to reset baselines you might have skipped.
  • Percy’s single-DOM-capture misses per-width JavaScript. Because Percy re-renders one captured DOM at each width, a component that runs a ResizeObserver to change its markup at a breakpoint will not reflect that JS-driven change at every width — the DOM was captured once. Components that restructure via CSS media queries are fine; those that restructure via JS need per-width capture.
  • Cross-browser multiplies both meters. Adding Firefox and Safari doubles or triples snapshot consumption on either tool. Reconcile the browser set here with your cross-browser matrix so you are not paying to re-capture browsers that never regress.

FAQ

Which has deeper Storybook integration, Percy or Chromatic?

Chromatic is built by the Storybook maintainers and treats each story as a first-class snapshot, reading story parameters such as chromatic.viewports and chromatic.disableSnapshot directly. Percy integrates through the @percy/storybook plugin, which enumerates stories and snapshots them, with per-story options set via a percy parameter. Both cover Storybook well; Chromatic’s coupling is tighter because story configuration and snapshot configuration are the same object.

How do Percy and Chromatic handle responsive-width capture?

Percy captures one DOM snapshot and re-renders it in its cloud at each width listed in the widths array, so extra widths are cheap re-renders of one capture. Chromatic captures a separate snapshot per viewport listed in chromatic.viewports, so each width is billed as its own snapshot. If you need many widths, Percy’s re-render model is usually more economical; if you need per-viewport browser accuracy, Chromatic’s discrete snapshots are more faithful.

How does baseline branching differ between Percy and Chromatic?

Chromatic ties baselines to git branches: each branch keeps its own accepted baseline and merging carries the approval to the target branch, so a feature branch never diffs against unrelated work. Percy compares against a configured base branch and auto-approves baselines on that branch after merge. Chromatic’s per-branch model reduces cross-branch noise; Percy’s is simpler to reason about when everyone targets one main branch.