Compare Website Screenshots for Visual Changes with Playwright
Use Playwright Test’s screenshot assertions to catch visual regressions, control rendering noise, review diffs, and keep baselines reliable in CI.
Use Playwright Test’s toHaveScreenshot() assertion to compare a page or element against a reference image. The first run creates the reference; later runs capture the same target and fail when the result differs beyond the configured threshold. For trustworthy results, make the page state deterministic, generate and review baselines in the same environment used for comparisons, and treat every baseline update as a code change. Playwright visual comparisons guide.
1. Install Playwright Test and create a screenshot comparison
For a new project, install Playwright Test and its browser binaries:
npm init playwright@latest
Choose JavaScript or TypeScript in the setup prompts. To add it to an existing Node.js project instead:
npm install --save-dev @playwright/test
npx playwright install
Create tests/visual.spec.js:
const { test, expect } = require('@playwright/test');
test('landing page matches its visual baseline', async ({ page }) => {
await page.goto('http://localhost:3000', { waitUntil: 'networkidle' });
await expect(page).toHaveScreenshot('landing.png');
});
Start the application before running the test. Then generate the baseline:
npx playwright test tests/visual.spec.js --update-snapshots
Inspect the generated reference image, commonly stored under a platform-specific directory next to the test, and commit the accepted snapshot with the test. On subsequent runs, omit --update-snapshots:
npx playwright test tests/visual.spec.js
The assertion waits until two consecutive screenshots match before comparing, which helps avoid capturing a page while it is still changing. Screenshot assertions disable animations by default. PageAssertions documentation.
2. Choose the comparison scope
Compare the viewport
The example above captures the page’s visible viewport. This is useful for a defined screen region or responsive layout check. Set the viewport explicitly in the test or project configuration so the baseline has a known size.
Compare the full page
Pass fullPage: true to capture the full scrollable page:
await expect(page).toHaveScreenshot('landing-full.png', {
fullPage: true,
});
Full-page images can be much taller and slower to compare. Pages with lazy-loaded sections may need to be scrolled or otherwise prepared so content below the fold is loaded before capture.
Compare a component
Use a locator assertion when you want to isolate a component from unrelated page changes:
await expect(page.getByTestId('pricing-card')).toHaveScreenshot('pricing-card.png');
A locator screenshot makes the component the comparison boundary. Prefer stable locators such as test IDs when text or layout changes are expected parts of the test.
3. Make page state repeatable
A visual snapshot is only useful when the tested page is in the intended state. Establish that state before taking the screenshot:
- Navigate to a stable local or test URL and wait for application-specific readiness, such as a heading or loaded component.
- Use a fixed viewport, locale, timezone, and test data where those affect rendering.
- Set a consistent color scheme and browser project. Keep the baseline and comparison runs on the same browser and operating system.
- Control animation, video, carousels, clocks, rotating content, and random values if they can change between runs.
- Use deterministic fonts and wait for them to load before capture if font loading affects layout.
Prefer a meaningful application readiness condition over relying only on a generic network event:
await page.goto('http://localhost:3000');
await page.getByRole('heading', { name: 'Plans' }).waitFor();
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('plans.png');
Use networkidle only when it suits the application. Long polling or background network activity can make it unsuitable, and a quiet network does not always mean the UI is ready.
4. Control dynamic content and comparison sensitivity
Mask small regions that are truly volatile, such as a live timestamp, or hide changing content with a screenshot stylesheet. Apply these controls narrowly: masking a large region or hiding a component can conceal a real regression.
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [page.getByTestId('live-clock')],
maskColor: '#808080',
});
You can also use a custom stylesheet to standardize volatile elements:
await expect(page).toHaveScreenshot('dashboard.png', {
style: `
[data-testid="live-clock"] {
visibility: hidden !important;
}
`,
});
Playwright’s screenshot assertion options include pixel count and ratio thresholds and a color threshold. Start with defaults, inspect actual diffs, and only relax a threshold when you have identified harmless rendering noise. A permissive threshold can let meaningful changes pass. See the SnapshotAssertions API for current option details.
5. Review and update reference images
When a comparison fails, inspect the expected, actual, and diff images. Decide whether the change is an application regression, intended design work, or an environment mismatch. If the change is intended, regenerate snapshots and review every resulting image:
npx playwright test --update-snapshots
Commit updated snapshots alongside the code or design change that explains them. Do not update all snapshots simply to make a failing run pass. The snapshot history is part of the review record. Playwright recommends reviewing and committing visual baselines.
6. Run comparisons consistently in CI
Browser rendering can vary with the host OS, browser version, settings, hardware, power source, and headless mode. Generate and compare baselines in the same environment where possible. If CI is the canonical comparison environment, create or update baselines there or in a matching container, and run the same browser project in each comparison.
Install the browser binaries that match the project’s Playwright version. If caching browser binaries in CI, key the cache to a hash of the Playwright version so a package upgrade does not reuse incompatible browsers. See Playwright CI guidance.
To make a failing CI run easier to diagnose, retain its Playwright report and trace artifacts according to your CI workflow. Open a trace with the Trace Viewer and inspect the expected, actual, and diff screenshots, as well as the browser and viewport metadata. Trace Viewer documentation.
7. Browser projects and coverage
Playwright supports Chromium, WebKit, Firefox, and branded Chrome and Edge. A baseline should be associated with the browser and platform for which it was created. Choose coverage based on the browsers your users rely on; do not compare output from one browser against another browser’s baseline. Playwright browser documentation.
For cross-browser coverage, define separate Playwright projects and let each project maintain its own snapshots. This catches browser-specific differences while keeping each comparison meaningful. Exact project configuration can vary by installed browser and test needs; use the Playwright projects guide when adding projects.
8. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshot differs on every run | Dynamic content, animation, random data, or incomplete page readiness. | Wait for a stable application condition; freeze or narrowly mask volatile content; inspect whether fonts or images finish loading before capture. |
| Diff appears only on CI | Different OS, browser version, fonts, headless mode, or rendering environment. | Use a matching CI image and Playwright browser version; regenerate baselines in the canonical environment and inspect trace metadata. |
| Snapshot not found | No reference exists for this test, browser project, or snapshot name. | Run the test with --update-snapshots, inspect the created image, and commit it if it is the intended baseline. |
| Full-page image misses lazy content | Content below the fold has not loaded or was not activated. | Scroll through the page or trigger the relevant UI before capture, then wait for the content’s ready condition. |
| Comparison fails after a browser update | Rendering or browser binaries changed. | Confirm the browser version and environment; review the visual diff, then update baselines only for accepted changes. |
| Test hangs waiting for network idle | Persistent requests keep the page network active. | Wait for a specific visible element or app-ready signal instead of network idle. |
| Too many harmless pixel failures | Minor platform rendering noise or unstable antialiasing. | First align environments and stabilize inputs. Then use a narrowly justified threshold; avoid broad tolerance that hides layout changes. |
9. Performance, reliability, and maintenance
- Keep the scope focused. Component snapshots are smaller and often easier to diagnose; full-page snapshots cover broader layout behavior but create larger artifacts.
- Reduce avoidable variability. Stable data, fonts, viewport, browser, and readiness conditions reduce reruns and ambiguous diffs.
- Keep baselines reviewable. Commit expected images and review changes with the code. Avoid mass snapshot updates without understanding why each changed.
- Use traces for diagnosis. A diff image shows where pixels changed; the trace and its metadata help identify whether the cause is application state or the rendering environment.
- Budget CI time around scope. More pages, browser projects, and full-page captures mean more screenshots to generate and compare. Start with high-value pages and components, then expand based on regressions the suite needs to catch.
Playwright’s built-in comparison has no separate per-screenshot service charge described in the cited documentation; operational cost comes from the machines and CI time used to run the suite and store its artifacts. Keep the suite small enough to run reliably in the feedback loop where it is useful.
10. Or skip the browser setup
Playwright is a good fit when the goal is to test your own application against committed baselines. For a one-off screenshot, a scheduled capture, or a workflow that should not install and maintain browser binaries, ScreenshotNeo provides a website screenshot API and an MCP server.
Make a single GET request to capture a URL. 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://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Cookie banners, newsletter popups, and chat widgets are removed before capture, and each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.
Frequently asked questions
Does Playwright compare screenshots automatically?
Yes. toHaveScreenshot() creates a reference when one does not exist and compares future captures against it.
Should visual snapshots run in every browser?
Run them in the browsers and platforms that matter to your users. Keep each baseline paired with the browser and environment that produced it.
Can I use this for a single component instead of a whole page?
Yes. Call toHaveScreenshot() on a locator to compare a focused element.
When should I update snapshots?
After confirming a visual change is intended, review the new images and commit them with the related change.


