Chromatic
02 / 02

Avoiding Flaky Diffs & Interaction Testing

Chromatic: Avoiding Flaky Diffs & Interaction Testing

Sources of False-Positive Diffs

// A snapshot captures ONE instant -- non-deterministic content produces
// diffs that reflect noise, not real regressions.

// BAD: live/random data -- changes every build regardless of code changes
export const LiveOrderList: Story = {
  render: () => <OrderList fetchOrders={fetchFromRealAPI} />,
};

// GOOD: fixed mock data -- deterministic, comparable across builds
export const OrderListWithData: Story = {
  args: {
    orders: [
      { id: '1', total: 42.0, createdAt: '2026-01-01T00:00:00Z' },
      { id: '2', total: 18.5, createdAt: '2026-01-02T00:00:00Z' },
    ],
  },
};

// BAD: an in-progress CSS animation -- caught at a different frame each run
.modal-enter { transition: opacity 300ms, transform 300ms; }

// Disable animations for the snapshot capture specifically
// .storybook/preview.ts
export const parameters = {
  chromatic: { pauseAnimationAtEnd: true },
};

Diff Threshold

// Sub-pixel anti-aliasing/GPU rounding differences can trigger spurious
// diffs even between two identical renders. diffThreshold tolerates a
// small amount of pixel noise while still catching real changes.
{
  "diffThreshold": 0.063
}

// Per-story override for a component known to render with more variance
// (e.g. one using a canvas or WebGL)
export const ChartStory: Story = {
  parameters: {
    chromatic: { diffThreshold: 0.2 },
  },
};

Interaction Testing

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

// A play function simulates user interaction inside the story --
// Chromatic can snapshot the RESULTING state, not just the initial render
export const DropdownOpened: Story = {
  render: () => <Dropdown options={['A', 'B', 'C']} />,
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.click(canvas.getByRole('button', { name: 'Open' }));
    await expect(canvas.getByRole('listbox')).toBeVisible();
  },
};
// Chromatic captures a snapshot AFTER the play function runs, extending
// visual coverage to interactive states, not just default appearance.

TurboSnap & Auto-Accept

  • TurboSnap (onlyChanged: true) uses git history + the module dependency graph to only re-snapshot stories actually affected by a commit's changes -- big cost/time savings on large libraries.

  • Requires full git history in CI (fetch-depth: 0 in GitHub Actions) to compute the diff correctly.

  • Auto-accepting on trunk/main is common -- the real review already happened via code review before merge, so the merged state just becomes the new baseline automatically.

  • A shared design system especially benefits: an unintended change to a base component fans out to every consuming app, so catching it here is far cheaper than after it ships.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free