Lost Pixel: Open-Source Visual Regression Testing

Lost Pixel: Open-Source Visual Regression Testing

Visual regression testing catches UI changes that unit tests miss. Lost Pixel is an open-source visual regression testing platform that compares screenshots of components or pages across commits, flags differences, and integrates with your existing CI pipeline. Unlike hosted-only tools, Lost Pixel can run entirely in your own infrastructure.

What Lost Pixel Does

Lost Pixel captures screenshots of your UI — components, pages, or Storybook stories — and compares them against a baseline. When pixels change, it surfaces those differences for review. Changes can be approved (updating the baseline) or rejected (treating them as regressions).

Two deployment modes:

  • Managed (lost-pixel.com) — screenshots stored in Lost Pixel's cloud, reviews in their UI
  • Open source (@lost-pixel/lost-pixel)— run yourself, store baselines in your repo or S3

This guide focuses on the open-source self-hosted approach.

Installation

npm install --save-dev @lost-pixel/lost-pixel

Create lostpixel.config.ts in your project root:

import { CustomProjectConfig } from '@lost-pixel/lost-pixel';

const config: CustomProjectConfig = {
  storybookShots: {
    storybookUrl: 'http://localhost:6006',
  },
  generateOnly: false,
  failOnDifference: true,
};

export default config;

Modes: Storybook, Custom Pages, and Full Pages

Storybook Integration

Lost Pixel discovers stories automatically from your running Storybook:

const config: CustomProjectConfig = {
  storybookShots: {
    storybookUrl: 'http://localhost:6006',
  },
};

Start Storybook, then run Lost Pixel:

npx storybook dev -p 6006 &
npx lost-pixel

Lost Pixel visits each story URL, takes a screenshot, and compares against the baseline.

Custom Page Screenshots

For testing specific pages or components rendered in a custom setup:

const config: CustomProjectConfig = {
  customShots: {
    currentShotsPath: './.lostpixel/current',
    shots: [
      {
        id: 'home-page',
        url: 'http://localhost:3000/',
        waitForSelector: '.hero-section',
      },
      {
        id: 'dashboard-logged-in',
        url: 'http://localhost:3000/dashboard',
        waitForSelector: '.dashboard-content',
        beforeScreenshot: async (page) => {
          // Custom setup — login, set cookies, etc.
          await page.evaluate(() => {
            localStorage.setItem('auth-token', 'test-token');
          });
          await page.reload();
        },
      },
      {
        id: 'button-hover-state',
        url: 'http://localhost:3000/components/button',
        waitForSelector: '.button',
        beforeScreenshot: async (page) => {
          await page.hover('.button');
        },
      },
    ],
  },
};

Ladle Integration

If you use Ladle instead of Storybook:

const config: CustomProjectConfig = {
  ladleShots: {
    ladleUrl: 'http://localhost:61000',
  },
};

Managing Baselines

Lost Pixel stores baseline screenshots in .lostpixel/baseline/ by default. These should be committed to your repository — they're the ground truth your changes are compared against.

First run creates baselines:

npx lost-pixel --generate-only
git add .lostpixel/baseline/
git commit -m "chore: add visual regression baselines"

Subsequent runs compare against the committed baseline.

Updating Baselines

When you intentionally change a component's appearance, update the baseline:

# Update all baselines
npx lost-pixel --generate-only

<span class="hljs-comment"># Or use the update flag
npx lost-pixel --update

git add .lostpixel/baseline/
git commit -m <span class="hljs-string">"chore: update visual baselines for new button design"

Configuration Options

Full lostpixel.config.ts:

import { CustomProjectConfig } from '@lost-pixel/lost-pixel';

const config: CustomProjectConfig = {
  storybookShots: {
    storybookUrl: process.env.STORYBOOK_URL || 'http://localhost:6006',
  },
  
  // Screenshot options
  screenshotOptions: {
    fullPage: false,
  },
  
  // Comparison thresholds
  threshold: 0,           // pixel-perfect (0 = no tolerance)
  // OR
  thresholdType: 'percent',
  threshold: 0.1,         // 0.1% of pixels can differ
  
  // Output paths
  currentShotsPath: './.lostpixel/current',
  baselineShotsPath: './.lostpixel/baseline',
  diffShotsPath: './.lostpixel/diff',
  
  // Mask elements (hide dynamic content like timestamps)
  mask: [
    { selector: '.timestamp' },
    { selector: '.user-avatar' },
  ],
  
  // Viewport
  breakpoints: [
    { width: 375, height: 812 },   // mobile
    { width: 1440, height: 900 },  // desktop
  ],
  
  // Fail behavior
  failOnDifference: true,
  generateOnly: false,
  
  // Concurrency
  shotConcurrency: 4,
};

export default config;

Handling Dynamic Content

Timestamps, user-specific data, and animations cause false positives. Three strategies:

Mask selectors — replace dynamic elements with a colored block in screenshots:

mask: [
  { selector: '.updated-at' },
  { selector: '[data-testid="random-avatar"]' },
],

Wait for stable state — wait for animations to complete:

shots: [
  {
    id: 'animated-component',
    url: 'http://localhost:6006/?story=loading-state',
    waitForSelector: '.content-loaded',  // wait for animation end
    beforeScreenshot: async (page) => {
      // Disable CSS animations
      await page.addStyleTag({
        content: '*, *::before, *::after { animation: none !important; transition: none !important; }',
      });
    },
  },
],

Mock data — serve deterministic data in test mode:

beforeScreenshot: async (page) => {
  await page.route('**/api/user', (route) => {
    route.fulfill({
      body: JSON.stringify({ name: 'Test User', joined: '2024-01-01' }),
    });
  });
},

CI Integration

GitHub Actions

name: Visual Regression

on: [pull_request]

jobs:
  visual-regression:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # needed for baseline comparison
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: npm
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build Storybook
        run: npm run build-storybook
      
      - name: Serve Storybook
        run: npx serve storybook-static -p 6006 &
      
      - name: Wait for Storybook
        run: npx wait-on http://localhost:6006
      
      - name: Run Lost Pixel
        run: npx lost-pixel
      
      - name: Upload diff screenshots
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: visual-regression-diffs
          path: .lostpixel/diff/

Failing on Differences

When failOnDifference: true (the default), Lost Pixel exits with code 1 when any screenshot differs. CI marks the run as failed.

The diff images in .lostpixel/diff/ show exactly what changed — upload them as artifacts for review.

Docker for Consistent Screenshots

Screenshots vary across operating systems and font rendering engines. For consistent baselines, generate them in a Docker container:

FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# GitHub Actions
- name: Run Lost Pixel in Docker
  run: |
    docker build -t visual-test .
    docker run --rm \
      -v $PWD/.lostpixel:/app/.lostpixel \
      visual-test npx lost-pixel

This ensures screenshots are always generated with the same fonts, rendering engine, and OS — eliminating environment-caused false positives.

Comparing Lost Pixel vs Percy vs Chromatic

Feature Lost Pixel Percy Chromatic
Open source Yes No No
Self-hosted Yes No No
Baselines in repo Yes No No
Storybook native Yes Yes Yes
Multi-browser Via Playwright Yes Yes
Price Free Usage-based Usage-based

Lost Pixel is the right choice when you want baselines version-controlled alongside code, need full data ownership, or want to avoid per-screenshot billing at scale.

Summary

Lost Pixel gives you visual regression testing without vendor lock-in:

  • Baselines live in your repo alongside code
  • Storybook, Ladle, and custom page support
  • Masking for dynamic content
  • Threshold configuration for tolerance
  • Full CI integration with artifact uploads for review

For teams that want Chromatic-style visual testing without the SaaS dependency, Lost Pixel is the open-source answer.

Read more