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