Pair visual checks with functional assertions
Start with functional coverage: does signup work, is the total correct, did the row get created. Semantic assertions are cheap to maintain and directly encode product intent. Visual checks earn their place only after that layer exists — they catch the regressions semantics can’t see: broken layouts, overlapping text, missing icons, collapsed sidebars.
When reviewing a new screenshot baseline, also check the behavior the screen represents. A checkout can look correct while calculating the wrong amount. Generated tests need the same combination of reviewed functional and visual assertions as handwritten tests.
Pull request
Start with the paths affected by the change.
scope the runSignup · checkout · access
Each scenario reports its own assertion and captured route.
one blockedReady or blocked
A reviewer sees why the release can proceed or what needs attention.
reviewable resultScope: full-page vs component snapshots
Full-page snapshots maximize recall and minimize signal-to-noise: one stray “Recommended for you” banner fails every shot it appears in, and baseline storage balloons into tens of megabytes per screen. Component-scoped (element) snapshots invert the tradeoff — tight blast radius, small diffs, but they only see what you aimed at.
The pragmatic split most teams land on: element-level shots for design-system components (buttons,
cards, forms, empty states), where changes should be intentional and reviewable; full-page shots
reserved for a handful of money screens — landing, pricing, checkout — reviewed on a slower cadence.
Name snapshots by surface and state (checkout/error-state.png), not by test id, so a
failing diff tells you where to look before you open anything.
Anti-flaking: fonts, animations, viewport locks
Control common sources of visual variation: late-loading fonts, animations, blinking carets, clocks, and changing remote images. Set a consistent viewport for each comparison. Choose additional viewports based on the layouts your customers use, rather than treating one desktop size as complete coverage.
// playwright.config.ts
use: { viewport: { width: 1440, height: 900 } };
// dashboard.spec.ts
await page.evaluate(() => document.fonts.ready);
await page.clock.install({ time: new Date('2026-06-16T10:00:00Z') }); // freeze time
await expect(page).toHaveScreenshot('dashboard.png', {
animations: 'disabled',
caret: 'hide',
mask: [page.getByTestId('live-ticker')], // dynamic regions opt out
maxDiffPixelRatio: 0.01,
});Stub the network for anything non-deterministic rather than masking blindly — a masked region is a blind spot forever, while a stubbed response is still rendered and reviewed. More general de-flaking technique lives in our flaky-tests root causes guide.
Thresholds that respect antialiasing
A zero-tolerance threshold will eventually fail because two CI runners rasterized a font two subpixels apart. Cross-machine antialiasing variance is a fact of life; budget for it. Set thresholds empirically per surface: full-page shots need tolerance for known rendering variance, while component shots can run tighter. Start from a recorded baseline and adjust only after reviewing real diffs.
Two thresholds beat one: a hard limit that fails the build, and a softer watch level that flags drift for review without blocking. Watch for distribution shifts too — many small diffs across a page usually mean a layout shift or font substitution, which no pixel ratio correctly prices. And if your team ships across browsers, expect WebKit to be the pickiest renderer; budget its thresholds separately rather than averaging.
Review baseline changes alongside the code
Baseline images are code. Treat their changes like code: updating a baseline happens through a
dedicated command (--update-snapshots locally), lands as a PR, and requires a human
who can articulate why the pixels changed — “marketing header redesign,” not “it looked
different.” If nobody can explain a diff, that’s exactly when you want to stop the line.
Show expected, actual, and difference images together during review. Store or retain baselines where the team can associate them with the relevant code change. Require an explanation before accepting a changed baseline, and keep the functional assertions that verify what the page does.
A pragmatic workflow
- Step 1. Functional/semantic suite green first. No screenshots until intent is asserted.
- Step 2. Lock determinism: viewport 1440×900, fonts awaited, animations disabled, clocks frozen, network stubbed.
- Step 3. Element shots for design-system components, tight thresholds.
- Step 4. Full-page shots for money screens, looser thresholds, PR-gated.
- Step 5. Review every baseline change like a code change; never auto-update.
- Step 6. Fold visual checks into your broader cadence per our regression testing strategies.
Built this way, visual regression adds the layer it’s uniquely good at — catching what assertions miss — without taxing every merge with noise.