Visual Regression — Lost Pixel
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/storybook-designer/visual-regression.md |
| Description | Not 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
pnpm add -D lost-pixellostpixel.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
# Start Storybook firstpnpm storybook &
# Run visual regressionpnpm test:visual # compare against baseline
# Update baselines (after intentional visual change)pnpm test:visual:update # regenerates all baselinesConfigure in package.json:
{ "scripts": { "test:visual": "lost-pixel", "test:visual:update": "lost-pixel --update-baseline" }}CI Integration
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:
- Default — the baseline state every component should maintain
- Dark mode —
DarkModestory withdata-theme="night" - Key variants — destructive, warning, success if they have distinct visuals
- Interactive states — disabled, loading, error (static renderings of these)
- Long content —
WithLongContentto 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:
- Run
pnpm test:visual:updateto generate the new baseline. - Commit both the old deletion and the new baseline file.
- Note in the PR description that baselines were intentionally regenerated.
Threshold Guidance
| Change type | Threshold action |
|---|---|
| Font rendering, sub-pixel AA differences | Raise threshold slightly (0.005) |
| Intentional design change | Update baselines, not threshold |
| Flaky due to animation | Disable animation in that story’s decorators |
| Dark mode off-by-one colors | Verify 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 regressionif (process.env.LOST_PIXEL) { document.documentElement.style.setProperty('--motion-duration', '0ms')}