A visual regression suite that catches every pixel change is an annoying suite that nobody maintains. The goal is a focused suite that catches real regressions without generating false positives. Here is how to build one that lasts.
What to Test Visually
High-value candidates:
- Marketing and landing pages — visual accuracy directly impacts conversions
- Design system components — a broken component affects every page that uses it
- Email templates — these are notoriously hard to test otherwise
- Data visualisations — charts and graphs where visual accuracy is the point
- Critical user flows — checkout confirmation, onboarding steps
Lower-value candidates:
- Highly dynamic pages with lots of real-time content
- Admin interfaces used only internally
- Pages where content changes frequently (news feeds, dashboards with live data)
Snapshot Granularity
Full page: Catches anything that changes but produces large, noisy diffs. Use for stable marketing pages.
Component: More focused, easier to review, easier to update. Use for design system components and reusable UI elements.
Region: Capture a specific area of the page. Useful when you need page-level context but want to exclude dynamic areas.
Keeping the Suite Healthy
Delete snapshots for removed components. Orphaned snapshot files create confusion and clutter the repository.
Update baselines in the same PR as intentional changes. When a developer redesigns a component, they should update the baseline snapshot in the same PR. This keeps the diff meaningful.
Run visual tests on the same OS in CI. Font rendering differs between macOS, Linux, and Windows. Run CI on Linux and have developers update baselines from the CI environment (not their local machine) to avoid OS-specific diffs.
Use consistent viewport sizes. Define standard viewports in your Playwright config and stick to them:
// playwright.config.ts
use: {
viewport: { width: 1280, height: 720 }
}Test in light and dark mode if your app supports it:
test('homepage - dark mode', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' })
await page.goto('/')
await expect(page).toHaveScreenshot('homepage-dark.png')
})Responding to Failures
When a snapshot test fails, the first question is: is this a bug or an intentional change?
- If it's a bug: fix the CSS/component and re-run
- If it's intentional: update the baseline with
--update-snapshotsand commit the new baseline
Never update baselines without looking at the diff. "The test was failing so I updated the snapshots" without reviewing the diff is how visual regressions slip into production.
Integrating with Code Review
Make visual diffs part of the code review process:
- Link Percy dashboard to your PR in the PR description
- Add a checklist item: "Visual diffs reviewed and approved"
- Require visual approval alongside code approval before merging
Visual testing is most valuable when it's part of the team's workflow, not a step one person runs occasionally.