Skip to content

Visual Regression — Lost Pixel

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/storybook-designer/visual-regression.md
DescriptionNot specified

Source Content

Visual Regression — Lost Pixel

The design system uses Lost Pixel for visual regression testing. NOT Chromatic (closed SaaS). Lost Pixel is open-source and runs against your own Storybook instance.

How It Works

Lost Pixel takes screenshots of every story (or a configured subset), compares them to baseline snapshots, and fails CI if pixels differ beyond the threshold. Baselines are committed to the repo.

Setup

Terminal window
pnpm add -D lost-pixel

lostpixel.config.ts:

import { CustomProjectConfig } from 'lost-pixel'
export const config: CustomProjectConfig = {
storybookShots: {
storybookUrl: 'http://localhost:6006',
},
// Which stories to snapshot — include light AND dark mode
pageShots: undefined,
threshold: 0.001, // 0.1% pixel diff tolerance
// Group baselines by story path
imagePathBaseline: '.lostpixel/baseline',
imagePathCurrent: '.lostpixel/current',
imagePathDifference: '.lostpixel/difference',
}

Running

Terminal window
# Start Storybook first
pnpm storybook &
# Run visual regression
pnpm test:visual # compare against baseline
# Update baselines (after intentional visual change)
pnpm test:visual:update # regenerates all baselines

Configure in package.json:

{
"scripts": {
"test:visual": "lost-pixel",
"test:visual:update": "lost-pixel --update-baseline"
}
}

CI Integration

.github/workflows/visual.yml
name: Visual Regression
on: [pull_request]
jobs:
visual-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for baseline comparison
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build Storybook
run: pnpm build-storybook
- name: Serve Storybook
run: npx http-server storybook-static --port 6006 &
- name: Run Lost Pixel
run: pnpm test:visual
env:
LOST_PIXEL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

What to Snapshot

Snapshot the most important stories per component. You don’t need every story — prioritize:

  1. Default — the baseline state every component should maintain
  2. Dark modeDarkMode story with data-theme="night"
  3. Key variants — destructive, warning, success if they have distinct visuals
  4. Interactive states — disabled, loading, error (static renderings of these)
  5. Long contentWithLongContent to catch overflow regressions

Skip stories with random data, timestamps, or network-dependent content.

Naming Baselines

Lost Pixel names baselines after the story ID: ComponentName--story-name.png. Keep story names stable — renaming a story deletes its baseline and creates a new one, which looks like a change in CI.

When you rename a story intentionally:

  1. Run pnpm test:visual:update to generate the new baseline.
  2. Commit both the old deletion and the new baseline file.
  3. Note in the PR description that baselines were intentionally regenerated.

Threshold Guidance

Change typeThreshold action
Font rendering, sub-pixel AA differencesRaise threshold slightly (0.005)
Intentional design changeUpdate baselines, not threshold
Flaky due to animationDisable animation in that story’s decorators
Dark mode off-by-one colorsVerify token values, not threshold

Disabling Animation for Snapshots

Lost Pixel can trigger animations mid-snapshot. Suppress them in stories that will be visually tested:

export const Default: Story = {
parameters: {
// Disable CSS transitions for snapshot stability
chromatic: { disable: false }, // Lost Pixel ignores this — use CSS instead
},
decorators: [
(Story) => (
<div style={{ '--motion-duration': '0ms' } as React.CSSProperties}>
<Story />
</div>
),
],
}

Or globally in .storybook/preview.tsx for snapshot runs:

// Suppress motion during visual regression
if (process.env.LOST_PIXEL) {
document.documentElement.style.setProperty('--motion-duration', '0ms')
}