Skip to main content

Visual Testing Strategy & Best Practices

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-snapshots and 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.