Open-Source Visual Regression Tools
Tool Overview
| Tool | Approach | Integration | CI-Ready | Best For |
|---|---|---|---|---|
| Playwright built-in | toHaveScreenshot() + pixel diff |
Native Playwright | Yes | Teams already using Playwright |
| BackstopJS | Puppeteer screenshots + pixel diff | Config file + CLI | Yes | Simple projects, quick setup |
| reg-suit | S3/GCS storage + image comparison | Plugin-based | Yes | Custom pipelines, self-hosted |
| Loki | Storybook screenshots + diff | Storybook addon | Yes | Component testing (Storybook users) |
| Lost Pixel | Next.js/Storybook screenshots | GitHub Action | Yes | Next.js and Storybook projects |
Playwright Built-in Visual Testing
Playwright includes screenshot comparison out of the box with no additional dependencies. This is the recommended starting point for most teams.
// tests/visual/playwright-native.spec.ts
import { test, expect } from '@playwright/test';
test('login page matches baseline', async ({ page }) => {
await page.goto('/login');
// Full page screenshot comparison
await expect(page).toHaveScreenshot('login-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.01, // Allow 1% pixel difference
threshold: 0.2, // Per-pixel color threshold (0-1)
animations: 'disabled', // Freeze animations for consistency
});
});
test('navigation component matches baseline', async ({ page }) => {
await page.goto('/');
// Component-level screenshot
const nav = page.locator('nav[data-testid="main-nav"]');
await expect(nav).toHaveScreenshot('main-navigation.png', {
maxDiffPixelRatio: 0.005,
});
});
test('modal dialog matches baseline', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="open-modal"]');
// Mask dynamic content before capturing
await expect(page).toHaveScreenshot('confirmation-modal.png', {
mask: [
page.locator('[data-testid="timestamp"]'),
page.locator('[data-testid="user-avatar"]'),
page.locator('[data-testid="order-id"]'),
],
});
});
Playwright Screenshot Configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
toHaveScreenshot: {
// Default comparison settings
maxDiffPixelRatio: 0.01,
threshold: 0.2,
animations: 'disabled',
},
},
// Store baselines in a dedicated directory
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
});
Updating Baselines
# Update all baselines (after intentional changes)
npx playwright test --update-snapshots
# Update baselines for specific tests
npx playwright test tests/visual/homepage.spec.ts --update-snapshots
# Review changes in a side-by-side report
npx playwright show-report
Handling Cross-Platform Differences
Playwright screenshots differ across operating systems due to font rendering. Handle this with platform-specific baselines:
// Playwright automatically stores OS-specific baselines:
// __screenshots__/login-page-chromium-linux.png
// __screenshots__/login-page-chromium-darwin.png
// __screenshots__/login-page-chromium-win32.png
// Or use a Docker container for consistent rendering:
// docker run --rm -v $(pwd):/work mcr.microsoft.com/playwright:v1.48.0-noble \
// npx playwright test --update-snapshots
BackstopJS
BackstopJS provides a configuration-driven approach to visual testing. Define scenarios in a JSON file, and BackstopJS handles screenshot capture, comparison, and reporting.
{
"id": "my-app-visual-tests",
"viewports": [
{ "label": "phone", "width": 375, "height": 812 },
{ "label": "tablet", "width": 768, "height": 1024 },
{ "label": "desktop", "width": 1440, "height": 900 }
],
"scenarios": [
{
"label": "Homepage",
"url": "http://localhost:3000/",
"delay": 1000,
"misMatchThreshold": 0.1,
"requireSameDimensions": true,
"hideSelectors": [".dynamic-timestamp", ".user-avatar"],
"removeSelectors": [".cookie-banner"]
},
{
"label": "Login Form",
"url": "http://localhost:3000/login",
"delay": 500,
"selectors": ["[data-testid='login-form']"],
"selectorExpansion": true,
"misMatchThreshold": 0.05
},
{
"label": "Dashboard - After Login",
"url": "http://localhost:3000/dashboard",
"delay": 2000,
"cookiePath": "test-data/auth-cookies.json",
"hideSelectors": [".chart-animation", ".live-counter"]
}
],
"engine": "playwright",
"report": ["browser", "CI"],
"ci": {
"format": "junit",
"testReportFileName": "backstop-results",
"testSuiteName": "Visual Regression"
}
}
BackstopJS Commands
# Create initial baselines
npx backstop reference
# Run comparison against baselines
npx backstop test
# Approve current screenshots as new baselines
npx backstop approve
# Open the visual report in a browser
npx backstop openReport
Advanced BackstopJS Features
{
"label": "Product Detail - Hover State",
"url": "http://localhost:3000/products/1",
"hoverSelector": ".add-to-cart-btn",
"postInteractionWait": 500,
"misMatchThreshold": 0.1
},
{
"label": "Mobile Menu Open",
"url": "http://localhost:3000/",
"viewports": [{ "label": "mobile", "width": 375, "height": 812 }],
"clickSelector": ".hamburger-menu",
"postInteractionWait": 500,
"scrollToSelector": ".menu-drawer"
},
{
"label": "Form Validation Errors",
"url": "http://localhost:3000/register",
"onReadyScript": "scripts/submit-empty-form.js",
"delay": 1000,
"selectors": ["[data-testid='registration-form']"]
}
Lost Pixel
Lost Pixel is designed specifically for Next.js and Storybook projects, with a GitHub Action for seamless CI integration.
# .github/workflows/lost-pixel.yml
name: Lost Pixel
on: [pull_request]
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build-storybook
- name: Lost Pixel
uses: lost-pixel/lost-pixel@v3
env:
LOST_PIXEL_API_KEY: ${{ secrets.LOST_PIXEL_API_KEY }}
// lostpixel.config.ts
import { CustomShot } from 'lost-pixel';
export const config = {
storybookShots: {
storybookUrl: './storybook-static',
},
pageShots: {
pages: [
{ path: '/', name: 'homepage' },
{ path: '/products', name: 'products' },
{ path: '/login', name: 'login' },
],
baseUrl: 'http://localhost:3000',
},
threshold: 0.05,
generateOnly: false,
};
Choosing the Right Tool
| Scenario | Recommended Tool | Rationale |
|---|---|---|
| Already using Playwright | Playwright built-in | Zero additional dependencies |
| Storybook-heavy workflow | Chromatic (commercial) or Loki (OSS) | Component-level testing |
| Simple project, quick setup | BackstopJS | Configuration-driven, no code needed |
| Next.js project | Lost Pixel | Built for Next.js + Storybook |
| Custom pipeline, self-hosted | reg-suit | Flexible storage backends |
| Need AI-powered diffing | Chromatic or Percy (commercial) | OSS tools use pixel diff only |
The open-source tools all share the same fundamental limitation: pixel-diff comparison. They will produce false positives on font rendering differences, anti-aliasing, and sub-pixel positioning. Commercial tools (Percy, Chromatic, Applitools) layer AI on top to reduce this noise. For most teams, starting with Playwright's built-in screenshots and upgrading to a commercial tool when false positives become painful is the right progression.