Commercial Visual Regression Tools
Percy (BrowserStack)
Percy captures screenshots across browsers and viewports, then highlights visual changes with smart diffing. It integrates with all major test frameworks and provides a web dashboard for reviewing and approving changes.
// tests/visual/homepage.spec.ts
import { test } from '@playwright/test';
import percySnapshot from '@percy/playwright';
test('homepage visual regression', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Capture at multiple widths
await percySnapshot(page, 'Homepage', {
widths: [375, 768, 1280],
minHeight: 1024,
percyCSS: `
/* Stabilize dynamic content for consistent screenshots */
[data-testid="timestamp"] { visibility: hidden; }
[data-testid="avatar"] { visibility: hidden; }
.animated { animation: none !important; }
`,
});
});
test('product card visual regression', async ({ page }) => {
await page.goto('/products');
// Wait for images to load
await page.waitForFunction(() => {
const images = document.querySelectorAll('img');
return Array.from(images).every(img => img.complete);
});
await percySnapshot(page, 'Product Listing', {
widths: [375, 1280],
});
});
test('checkout flow visual regression', async ({ page }) => {
// Navigate through checkout steps
await page.goto('/cart');
await percySnapshot(page, 'Cart Page', { widths: [375, 1280] });
await page.click('[data-testid="proceed-to-checkout"]');
await percySnapshot(page, 'Checkout - Shipping', { widths: [375, 1280] });
await page.fill('[data-testid="shipping-name"]', 'John Doe');
await page.fill('[data-testid="shipping-address"]', '123 Main St');
await page.click('[data-testid="continue-to-payment"]');
await percySnapshot(page, 'Checkout - Payment', { widths: [375, 1280] });
});
Percy CI Configuration
# .github/workflows/percy.yml
name: Percy Visual Tests
on: [pull_request]
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- name: Run Percy snapshots
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
run: npx percy exec -- npx playwright test tests/visual/
Percy's approval workflow: screenshots from the PR branch are compared against the base branch. Unchanged screenshots are auto-approved. Changed screenshots appear in the Percy dashboard for review. Approved changes become the new baseline when the PR merges.
Chromatic (Storybook)
Chromatic integrates directly with Storybook to test components in isolation. This catches visual regressions at the component level before they compound into page-level issues.
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
// Chromatic-specific configuration
chromatic: {
viewports: [320, 768, 1200],
delay: 300, // Wait for animations
diffThreshold: 0.063, // Sensitivity (default is 0.063)
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: 'primary', children: 'Click me' },
};
export const Disabled: Story = {
args: { variant: 'primary', children: 'Disabled', disabled: true },
};
export const Loading: Story = {
args: { variant: 'primary', children: 'Loading', loading: true },
parameters: {
chromatic: {
// Capture after loading spinner appears
delay: 500,
},
},
};
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
</div>
),
};
Chromatic CI Integration
# In your CI pipeline
- name: Run Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitOnceUploaded: true # Don't wait for review
onlyChanged: true # Only snapshot changed stories
traceChanged: true # Detect which stories are affected by code changes
Applitools Eyes
Applitools uses a proprietary AI engine ("Visual AI") that provides the most sophisticated intelligent comparison. It can test across multiple browsers and devices simultaneously from a single test run.
// tests/visual/applitools-checkout.spec.ts
import { test } from '@playwright/test';
import {
Eyes, Target, BatchInfo, Configuration,
BrowserType, DeviceName, ScreenOrientation
} from '@applitools/eyes-playwright';
test.describe('Checkout Flow Visual Tests', () => {
let eyes: Eyes;
test.beforeEach(async () => {
eyes = new Eyes();
const config = new Configuration();
config.setBatch(new BatchInfo('Checkout Flow'));
// Test across multiple browsers and devices simultaneously
config.addBrowser(1200, 800, BrowserType.CHROME);
config.addBrowser(1200, 800, BrowserType.FIREFOX);
config.addBrowser(1200, 800, BrowserType.SAFARI);
config.addDeviceEmulation(DeviceName.iPhone_15, ScreenOrientation.PORTRAIT);
config.addDeviceEmulation(DeviceName.Pixel_7, ScreenOrientation.PORTRAIT);
eyes.setConfiguration(config);
});
test('cart page visual check', async ({ page }) => {
await eyes.open(page, 'E-Commerce', 'Cart Page');
await page.goto('/cart');
// Full page check with layout matching (ignores text content changes)
await eyes.check('Cart with items', Target.window()
.fully()
.layout() // Match layout, not exact content
.ignoreDisplacements() // Ignore minor position shifts
);
// Specific region check with strict matching
await eyes.check('Price total', Target.region('#order-summary')
.strict() // Exact match for financial data
);
await eyes.close();
});
test.afterEach(async () => {
await eyes.abort();
});
});
Applitools Match Levels
| Match Level | What It Compares | Use Case |
|---|---|---|
| Strict | Pixel-level, AI-filtered | Financial data, legal text, exact content |
| Layout | Element positions, sizes, relationships | Pages with dynamic content (user names, dates) |
| Content | Text content and structure | Pages where styling may vary but content matters |
| Ignore colors | Layout + content, ignoring color changes | Theming changes, dark mode testing |
Tool Comparison Matrix
| Feature | Percy | Chromatic | Applitools |
|---|---|---|---|
| AI-powered diff | Basic | Basic | Advanced (Visual AI) |
| Cross-browser rendering | Yes (cloud rendering) | Yes (cloud rendering) | Yes (Ultrafast Grid) |
| Storybook integration | Via Percy CLI | Native (built for Storybook) | Via SDK |
| Playwright integration | Native | Via Storybook | Native SDK |
| Match sensitivity control | Threshold percentage | Threshold percentage | Multiple match levels |
| Approval workflow | Web dashboard | Web dashboard | Web dashboard |
| Free tier | 5,000 snapshots/month | 5,000 snapshots/month | Limited |
| Best for | Full-page testing, multiple frameworks | Storybook-centric workflows | Enterprise, cross-browser at scale |
| Pricing | Per snapshot | Per snapshot | Per checkpoint |
Choose based on your workflow: Chromatic if you use Storybook extensively, Percy if you need broad framework support, and Applitools if you need the most sophisticated AI comparison and cross-browser rendering.