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-pixelCreate 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-pixelLost 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-pixelThis 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.