How to set up screenshot-based visual regression testing in Playwright
Set up Playwright screenshot assertions, review and commit baselines, control dynamic content, and tune comparisons for reliable visual regression checks.
Use Playwright Test’s built-in expect(page).toHaveScreenshot() assertion to compare a rendered page with a reviewed reference image. The first run creates the baseline; later runs report visual differences. Keep the snapshots in version control, generate and compare them in a consistent environment, and review every baseline update.
Screenshot assertions are a Playwright Test runner feature. They are not available as standalone assertions when using Playwright’s library without its test runner. See the official Visual comparisons guide, page assertion API, and configuration reference.
1. Install Playwright Test
For a new project, install the test runner and its browser binaries:
npm init -y
npm install --save-dev @playwright/test
npx playwright install
If the project already uses Playwright Test, install the browser binaries required by its configured projects. In CI, install operating-system dependencies as well if the runner image requires them; the official install command supports --with-deps on supported Linux environments.
npx playwright install --with-deps
Commit your package manifest and lockfile so local and CI installs resolve the same Playwright version. A browser update can change rendering and snapshot output, so treat browser upgrades as baseline-affecting changes.
2. Add a screenshot assertion
Create a test such as tests/visual.spec.ts:
import { test, expect } from '@playwright/test';
test('landing page visual layout', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('landing.png');
});
Replace the example URL with a stable route in your application. A named screenshot makes the assertion’s purpose clear. Playwright stores screenshot references in its snapshot directory, normally beside the test using a generated directory name. If you omit the name, it derives one from the test and snapshot sequence.
For the first run, create and inspect the reference:
npx playwright test tests/visual.spec.ts
The initial run reports that the snapshot is missing and writes an actual screenshot. Playwright captures until two consecutive screenshots match, then saves the last image as the reference. Open the generated image and make sure it represents the intended appearance before committing it. Commit the snapshots with the test so subsequent runs have a baseline to compare.
3. Keep baseline and CI rendering consistent
Screenshot pixels depend on the rendering environment. Playwright’s documentation advises running tests in the same environment where the baseline was generated. Operating system, browser build, installed fonts, browser settings, hardware, power source, and headless mode can all affect output.
For dependable comparisons:
- Generate and compare references in the same operating system image where practical.
- Use the same Playwright version, browser binaries, project configuration, viewport, device scale factor, and fonts.
- Prefer generating updated baselines in the same CI image that will validate them.
- Keep test data, locale, timezone, and application state deterministic.
- Use the same headless or headed mode for baseline generation and comparison.
A project configuration can make the browser and test location explicit:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1440, height: 900 },
},
},
],
});
Project names are included in snapshot names when multiple projects are configured. Keep references for different browsers or environments separate: compare like with like instead of treating browser rendering differences as application regressions.
4. Choose page, full-page, or component scope
Match the screenshot assertion to the behavior under test:
- Viewport page:
toHaveScreenshot()checks the visible page area. Use it for above-the-fold layout or a particular viewport state. - Full page: pass
fullPage: trueto include the page’s full scrollable height. - Component: call the assertion on a locator to check one component without making the entire page’s appearance part of that test.
import { test, expect } from '@playwright/test';
test('pricing component', async ({ page }) => {
await page.goto('https://example.com/pricing');
const pricing = page.locator('[data-testid="pricing"]');
await expect(pricing).toHaveScreenshot('pricing-component.png');
});
test('entire article page', async ({ page }) => {
await page.goto('https://example.com/article');
await expect(page).toHaveScreenshot('article-full.png', {
fullPage: true,
});
});
Locator assertions first need the element to exist and be visible. Use a stable selector such as a test ID rather than a brittle positional selector. A full-page image can be tall and may expose content that is irrelevant to the behavior under test; choose the narrowest scope that still proves the intended visual behavior.
5. Make dynamic pages repeatable
Playwright reduces some transient differences by waiting until two consecutive page screenshots match. Screenshot assertions disable animations by default: finite animations are fast-forwarded and infinite animations are canceled for capture, then resume afterward. The caret is hidden by default.
These defaults do not make changing application data deterministic. Timestamps, rotating promotions, random avatars, live counts, personalized content, and third-party widgets can still change between runs. Prefer controlling the page’s data and state where possible. If a region is deliberately outside the test’s claim, mask it:
import { test, expect } from '@playwright/test';
test('dashboard layout', async ({ page }) => {
await page.goto('https://example.com/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [page.locator('[data-testid="live-clock"]')],
});
});
Mask only content whose appearance is irrelevant. A masked region no longer checks whether that content is styled or positioned correctly.
For more control, provide a stylesheet with stylePath. It can hide dynamic elements or adjust them just for capture. The stylesheet can affect Shadow DOM and inner frames:
await expect(page).toHaveScreenshot('dashboard.png', {
stylePath: './tests/visual-snapshot.css',
});
/* tests/visual-snapshot.css */
[data-testid="live-clock"],
[data-testid="rotating-promo"] {
visibility: hidden !important;
}
Use hiding or styling only when the changed content is not part of the behavior being verified. If layout depends on that element, hiding it can conceal real regressions.
6. Tune how much difference is acceptable
Playwright uses pixelmatch for screenshot comparison. Three controls answer different questions:
| Option | What it controls | Documented default |
|---|---|---|
threshold |
Per-pixel perceived color difference required for a pixel to count as different. Lower is stricter. | 0.2 |
maxDiffPixels |
Maximum number of pixels allowed to differ. | Unset |
maxDiffPixelRatio |
Maximum fraction of the screenshot allowed to differ. | Unset |
Start with the defaults. Inspect the expected, actual, and diff images when a test fails. Adjust the per-pixel threshold only if harmless color rendering variation is the issue; use a maximum pixel count or ratio when a small total changed area is acceptable. These are separate controls, and there is no universal tolerance that suits every application.
Set shared screenshot defaults in Playwright configuration, then override them for a specific assertion only when that test needs a different tolerance:
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
},
},
});
100 is an example value, not a general recommendation. A broad tolerance can let meaningful visual changes pass unnoticed. Review diffs before increasing it, and prefer fixing nondeterministic rendering when that is the source of noise.
7. Review and update references safely
When an intentional UI change causes a failure, inspect the diff, confirm the new appearance is correct, and then update the reference:
npx playwright test --update-snapshots
Review the changed PNG or WebP files alongside the application change and commit them together. Do not make snapshot updates an automatic reaction to every failure: the reference defines expected behavior, and blindly replacing it can bless an unintended regression.
In a pull request, make snapshot changes easy to inspect. Keep the test name and snapshot name descriptive, include the related UI change, and ensure reviewers can access the changed image artifacts. In CI, run the ordinary test command without the update flag so a mismatch fails the job instead of rewriting its expected image.
8. Configure snapshot storage only when needed
The default snapshot structure is usually simplest. Playwright includes browser and platform information in generated names, and uses a project name when multiple projects exist. Configure custom or shared snapshot locations only if the default per-test organization does not fit the repository.
import { defineConfig } from '@playwright/test';
export default defineConfig({
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
});
Choose a path convention that keeps each test’s reference unambiguous across projects and test files. Whatever layout you choose, commit the expected images and avoid sharing one baseline among configurations that render differently.
9. Run the suite in CI
Use the same locked dependencies and browser build in CI as in the baseline environment. A basic workflow command is:
npm ci
npx playwright install --with-deps
npx playwright test
For large suites, Playwright’s test runner supports normal test configuration and sharding. Keep screenshot tests deterministic before adding parallelism; tests that share mutable data or depend on timing can produce inconsistent images independent of the comparison settings.
CI should fail on an unexpected mismatch and preserve the expected, actual, and diff images as artifacts when available in your workflow. Artifact retention and upload configuration depend on the CI provider; the Playwright test itself does not require a separate screenshot service.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshot missing on the first run | No reference exists yet. | Inspect the generated actual image, then commit it as the reviewed baseline. |
| Images differ only in CI | OS, browser version, fonts, viewport, scale factor, headless mode, or browser settings differ. | Generate and compare references in a matching environment and configuration. |
| Flaky differences in a timestamp or live widget | Page data changes between captures. | Control test data; otherwise mask or style only the irrelevant region. |
| Locator screenshot times out | The locator is absent, hidden, or not uniquely/consistently targeted. | Wait for the intended state and use a stable test ID or selector. |
| Large page assertion is slow or hard to review | The full scrollable page includes much more content than the test needs. | Assert on a locator or viewport if that scope answers the test’s question. |
| Small rendering differences cause frequent failures | Font rasterization or other environment variation remains. | Align the environment first; then tune threshold or a total-difference cap narrowly. |
| Updating snapshots makes failures disappear without explanation | The baseline was overwritten without reviewing the change. | Restore the previous reference, inspect the diff, and update only after confirming the new UI is intended. |
| Assertion is unavailable in a script using Playwright directly | toHaveScreenshot() is an assertion from Playwright Test. |
Run it under @playwright/test, or implement a separate image comparison workflow for a non-test-runner script. |
11. Performance, reliability, and cost
Visual assertions add browser rendering and image comparison work to the test suite. Full-page captures produce larger images and cover more content than a focused locator capture. Keep the screenshot scope tight, avoid redundant assertions for identical states, and use a stable browser project when that is sufficient for the behavior under test.
Reliability comes primarily from repeatable inputs: stable data, consistent browser and OS, fixed viewport and fonts, and reviewed snapshots. Raising the diff tolerance can reduce noise but also weakens the check. Masking reduces sensitivity in the masked region, so reserve it for content outside the test’s purpose.
The Playwright workflow uses your test environment and repository snapshots; the cited documentation does not specify a per-screenshot service charge. Account for CI compute time, browser installation, and storage for committed image references in your own environment.
Or skip the browser setup
For captures outside a Playwright regression suite, ScreenshotNeo is a website screenshot API and MCP server. It can return PNG, JPEG, WebP, or PDF from one GET request. See the ScreenshotNeo API documentation for parameters and response details.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can I use JPEG references?
Playwright’s screenshot references are lossless PNG by default; use the .webp extension for lossless WebP. Keep the format consistent for a given baseline.
Should I check every browser?
Configure browser projects when cross-browser rendering is part of the behavior you need to protect. Each project should compare against its own appropriate references.
Do I need a hosted screenshot service for Playwright visual tests?
No. Playwright Test captures and compares screenshots within the test workflow. A screenshot API can serve separate capture needs, but it does not replace the runner’s baseline review process.


