All posts
4 min readIgor Luchenkov

How I cut my Chromatic bill 10x

A practical guide to slashing your visual regression bill. Every snapshot tool bills per story, so combine a component's variants into one gallery story and drive its data states from props to cut snapshot count by up to 10x with the same coverage. Works with Chromatic, Percy, and Playwright.

StorybookChromaticVisual testingCost

I have been a huge Storybook and Chromatic fan for years. But at some point the bill got my attention, and when I looked into why, the fix turned out to be simple. This is the write-up of what I changed. It works on any per-snapshot tool, not just Chromatic.

First, some backstory on how I got here, because it explains why the cost crept up in the first place.

How I ended up paying for a lot of snapshots

In the past I would build a gigantic end-to-end pipeline that was flaky as hell and made me spend time every week fixing it. It took 40 minutes to run, and when it went red someone would assume it was just flaky, merge the change anyway, and then find out it truly did break the system.

So I stopped writing lots of E2Es and moved to Storybook for interaction and visual testing. Much better. But because I was rendering every state of every component as its own story to get the screenshots in place, I was generating a lot of screenshots. And every snapshot tool, Chromatic, Percy, Playwright screenshots, UI Verify, renders and bills per story. So the number of stories is the cost, and it is also the noise surface: more stories means more places for a diff to flake.

I ended up paying a lot, which made me think about whether there were ways to optimise it. There were. Here they are.

The core idea: combine states into one story

The naive pattern is one story per variant times state times theme. A component with 5 sizes, 3 states, and 2 themes is 30 snapshots the naive way. The whole idea below is to collapse that matrix into a handful of stories while keeping full coverage.

Move 1: one gallery story, not N stories

For something like a Button, there is no need to have separate Primary, Secondary, and Tertiary stories. I prefer one AllVariants story that maps through the prop combinations and renders them in a grid. One snapshot then covers the entire matrix. As a bonus you get a nice grid that shows every permutation at a glance, with no extra clicks to see the variations.

Button.stories.tsx
// ONE story covers every size x variant
export const AllVariants = () => (
  <div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(3, max-content)' }}>
    {(['sm', 'md', 'lg'] as const).flatMap((size) =>
      (['primary', 'secondary', 'danger'] as const).map((variant) => (
        <Button key={`${size}-${variant}`} size={size} variant={variant}>
          {size}/{variant}
        </Button>
      )),
    )}
  </div>
);
// 9 buttons, 1 snapshot instead of 9.

Move 2: drive states from data, not from N stories

Same idea for a component that loads data. Instead of a Loading, Empty, and Error story each, I add an optional prop like storybookData and pass the response shape straight in. Then one AllStates story maps through the shapes and renders the component in each state. Deterministic input in, deterministic pixels out, many states, few shots.

List.stories.tsx
const CASES = [
  { label: 'empty', items: [] },
  { label: 'one',   items: [fixtures.one] },
  { label: 'many',  items: fixtures.many },
  { label: 'error', error: 'Failed to load' },
];

export const AllStates = () => (
  <div style={{ display: 'grid', gap: 24 }}>
    {CASES.map((c) => (
      <section key={c.label}>
        <h4>{c.label}</h4>
        <List items={c.items} error={c.error} />
      </section>
    ))}
  </div>
);

The tradeoff: say it out loud

Consolidating into one snapshot is coarser granularity. A diff anywhere in the gallery flags the whole story, and you lose the per-variant baseline and accept. So this is a balance, not merge everything.

  • Merge variants that change together and share one component: the size, state, and theme matrix of one Button. This is where consolidation wins cleanly.
  • Keep separate genuinely independent surfaces: different pages, different flows, things that evolve on their own timelines. Collapsing those just couples unrelated diffs.
Rule of thumb: merge a single component's own matrix, keep independent pages and flows apart. A tool with per-region diffing softens the coarseness because it can tell you which cell in the gallery moved.

The result

Applying these two moves across the codebase easily reduced the number of stories I had by roughly 10x. On the components with the worst explosion the win is even bigger: a size times variant times theme matrix plus four data states can go from 28 stories to 2 snapshots, about 93% fewer billed shots, with the same coverage.

Same pixels under test, a fraction of the bill, and fewer places to flake. If you are actively writing stories and do not want to burn money, this is the cheapest change you can make.

Make it automatic

I got tired of remembering to do this by hand, so I packaged the whole pattern as a coding-agent skill. Install it and your agent writes stories the economical way from the start, and refactors the exploding ones for you. It is free, and it is tool-agnostic: it helps whether you are on Chromatic, Percy, Playwright screenshots, or UI Verify.

Related skill

Write economical stories

Full visual coverage in the fewest billable snapshots.

UI Verify
Built by the team behind UI Verify

Let your agent write economical stories for you

UI Verify captures every component on every pull request and an AI judge tells an intended change from a real regression. See how it works.

© 2026 UI Verify