Visual Regression Testing
toHaveScreenshot compares a live screenshot to a committed golden image pixel by pixel. Visual regression catches unintended UI changes across components, pages and full flows — but only with strict controls on fonts, animation and anti-aliasing.
What is this?
Visual regression testing captures screenshots and compares them to baseline 'golden' images stored in the repo. Playwright's toHaveScreenshot re-takes the shot, compares it pixel-by-pixel with configurable tolerance, and fails the test with a human-readable diff image when they diverge. Golden images are updated intentionally with --update-snapshots.
Why do you need it?
- Functional assertions miss styling bugs: a shifted layout, a broken icon or a regressed font passes every logic test.
- Golden images give designers and reviewers a commit-reviewed record of what each screen looked like.
- Pixel diffs surface unintentional changes across browsers, breakpoints and themes in seconds.
- toHaveScreenshot retries until the image stabilizes (animations settled), reducing false positives.
- The diff images produced on failure make triage fast and visual, not a wall of assertion text.
Mental model
Comparing a signed print to the gallery's master print
toHaveScreenshot vs the golden image
The gallery measures the print against the master under controlled light. Playwright compares the rendered page against the committed golden under controlled fonts, viewport and animations — and flags any deviation with a marked-up diff.
How it works
- Run tests with toHaveScreenshot first time — Playwright captures the page and writes a golden PNG into the snapshots directory.
- On later runs it re-captures the same viewport and compares pixel colors with the golden image.
- Differences beyond the tolerance (maxDiffPixelRatio / maxDiffPixels) fail the test and produce an actual-vs-expected-vs-diff image set.
- Playwright waits for two consecutive frames of stability before capturing, which drops most animation jitter.
- Running npx playwright test --update-snapshots replaces goldens after an intentional, reviewed change.
The visual comparison loop behind toHaveScreenshot.
Key commands
npx playwright test --update-snapshotsRe-capture every golden image, replacing the committed baselines.
3 snapshot(s) updated.
await expect(page).toHaveScreenshot()Compare the full page screenshot to the golden image.
await expect(locator).toHaveScreenshot('name.png')Compare a single element's screenshot to a named golden.
npx playwright test --update-snapshots --grep 'pricing'Update only the snapshots matching a filter, so unrelated goldens stay untouched.
maxDiffPixelRatio: 0.02Tolerate up to 2% of pixels differing before the test fails.
animations: 'disabled'Pause CSS animations before capturing so the frame is deterministic.
Examples
A visual regression check for a pricing card
import { test, expect } from "@playwright/test"; test.use({ viewport: { width: 1280, height: 800 }, colorScheme: "light",}); test("pricing card matches the golden image", async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await page.goto("https://example.com/pricing"); const card = page.getByTestId("pricing-card"); await expect(card).toBeVisible(); await expect(card).toHaveScreenshot("pricing-card.png", { maxDiffPixelRatio: 0.02, animations: "disabled", });});test.use pins the viewport and color scheme so the golden image is captured under reproducible conditions. emulateMedia disables animations, and the screenshot options disable animations again and allow a 2% pixel tolerance for font smoothing and subtle anti-aliasing differences. The golden is compared and stored under the test name's snapshots folder.
In production
- Component screenshot suites (buttons, cards, nav) catch regressions before they reach a page-level test.
- Cross-browser goldens per project catch WebKit vs Chromium rendering drift that logic tests miss.
- Theme and dark-mode goldens verify every screen against both palettes on every commit.
- PR screenshots generated on the diff images give designers a visual review gate without running the app.
Best practices
- Pin viewport, device scale factor, color scheme and timezone in test.use so goldens are reproducible.
- Disable animations (reducedMotion/animations: 'disabled') and accept font loading before capturing.
- Keep maxDiffPixelRatio tight (0–2%) and raise it deliberately for known anti-aliasing variance, never to hide bugs.
- Review snapshot diffs in every PR; update goldens only after confirming the change is intentional.
- Scope screenshots to elements or regions instead of full pages to minimize rendering variance.
Common mistakes
- Running --update-snapshots after any failure, blindly overwriting goldens that actually caught a real regression.
- Capturing full-page shots of pages with dynamic ads, dates or avatars, producing permanent flakiness.
- Forgetting to pin the viewport, so local vs CI rendering sizes diverge and every run diffs.
- Comparing against goldens generated with a different browser version, causing systemic pixel drift.
Common issues
Screenshot comparison failed: 3.5% of pixels differFont rendering, shadows or anti-aliasing vary between the baseline machine and the current one.
Generate goldens on the same OS/browser used in CI, disable subpixel variations via forced fonts, and adjust maxDiffPixelRatio deliberately.
Snapshot 'pricing-card' is missing, wrote it to diskThis is the first run or the golden was deleted.
That's expected on the first run — review and commit the newly written golden image.
Timed out waiting for screenshot to stabilizeAn endless animation, carousel or live clock keeps the page from being frame-stable.
Mock the animated widget, freeze time with clock.setFixedTime or page.clock, and keep animations disabled in config.
Debugging tips
- Open the three generated images on failure: expected, actual and the diff with changed regions highlighted.
- If the whole image shifts by a constant amount, suspect viewport or scroll position rather than a UI bug.
- Zoom into the diff PNG to see whether changes are text (fonts/OS rendering) or layout (real regressions).
- Generate goldens in CI (not just locally) so OS font rendering matches production's expected baseline.
Interview checkpoint
Hands-on lab
- Capture a golden image for a pricing card
- Introduce a deliberate UI change and observe the pixel diff
- Use diff images to triage a failed comparison
- Update goldens intentionally with a filtered update
Golden Images for a Pricing Page
45 min · advanced
Quick quiz
Which option limits how different a screenshot can be before failing?
You should run --update-snapshots to confirm every unexpected screenshot failure is a real bug.
A golden was captured on a Mac with Retina rendering, and CI on Linux diffs on every run. What is the root cause?
Cheat sheet
await expect(page).toHaveScreenshot()Full-page goldenawait expect(locator).toHaveScreenshot('card.png')Element golden--update-snapshotsRegenerate goldens--update-snapshots --grep 'pricing'Regenerate a subsetmaxDiffPixelRatio: 0.02Tolerate 2% pixel driftanimations: 'disabled'Freeze CSS animationstest.use({ viewport, colorScheme })Pin rendering inputspage.emulateMedia({ reducedMotion })Disable motionpage.clock.setFixedTime(date)Freeze clocks/widgetspage.route('**/ads/*', abort)Strip dynamic content