Playwright guide

Visual regression testing with Playwright: toHaveScreenshot() explained

Set up visual regression testing in Playwright with toHaveScreenshot(): baselines, full-page captures, masking dynamic content, thresholds and CI tips.

Updated October 1, 20268 minute readBy the VisualRunner team

Playwright Test ships with built-in screenshot comparison. The first run writes a baseline image next to your test; later runs compare against it and fail when pixels differ beyond your threshold. Here is how to set it up so it stays reliable.

✓Built into Playwright Test
✓Baselines stored in the repo
✓Mask dynamic regions
✓Run the same OS in CI
1

A minimal visual test

Navigate to the page and assert on a screenshot. Playwright disables CSS animations for screenshot assertions by default and retries until two consecutive captures match, which removes a lot of flakiness.

import { test, expect } from '@playwright/test';

test('homepage looks right', async ({ page }) => {
  await page.goto('https://example.com/');
  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
    mask: [page.locator('[data-testid="live-date"]')],
    maxDiffPixelRatio: 0.01,
  });
});
2

Create and update baselines

The first run fails because no baseline exists and writes one. Review it, commit it, and run again. When a change is intentional, regenerate the baselines and review the image diff in the pull request.

npx playwright test                     # compare with baselines
npx playwright test --update-snapshots  # accept intentional changes
npx playwright show-report              # inspect expected / actual / diff
3

Keep screenshots deterministic

Baselines are platform-specific: fonts and anti-aliasing differ between macOS, Windows and Linux, so generate baselines in the same environment CI uses, typically the official Playwright Docker image. Then remove sources of noise in the page itself.

  • Mask dates, counters and ads with mask
  • Dismiss cookie banners before capturing
  • Wait for web fonts and lazy images
  • Pin viewport size in the project config
4

Cover multiple viewports

Define projects for desktop and mobile devices in playwright.config.ts. Each project keeps its own baselines, so the same test produces one screenshot per viewport.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'desktop', use: { ...devices['Desktop Chrome'] } },
    { name: 'mobile', use: { ...devices['Pixel 7'] } },
  ],
});
5

Where Playwright visual tests stop

Baselines live in git, review happens in pull-request diffs, and coverage is limited to the pages you wrote tests for. Large page counts, many languages and non-developer reviewers make that hard to scale.

6

Or skip the test code entirely

Code-based visual tests only cover the pages and states someone wrote a test for, and only run when the pipeline runs. VisualRunner captures production pages from a sitemap, crawl or URL list, across languages and viewports, and monitors them on a schedule — so CMS edits, translations and third-party scripts are caught too. Engineers can still trigger a monitor from CI through the API.

Questions

Frequently asked questions

Does Playwright have built-in visual regression testing?

Yes. expect(page).toHaveScreenshot() and expect(locator).toHaveScreenshot() compare screenshots with stored baselines in Playwright Test.

Why do my Playwright screenshots differ in CI?

Rendering differs between operating systems. Generate and compare baselines in the same environment, for example the Playwright Docker image.

How do I ignore dynamic content?

Pass locators to the mask option, or hide elements with the stylePath option before the screenshot is taken.

Continue exploring

Related resources

Turn the checklist into a repeatable workflow

Discover your pages, capture the important states and keep a visual history of every meaningful change.

Start free trial