How to Visual Test a UI with Playwright
Add reliable visual regression checks with Playwright Test: create and review baselines, control screenshot noise, tune comparisons, and debug failures.
How do I add visual comparison testing to a Playwright test? Use Playwright Test’s expect(page).toHaveScreenshot() to compare a page or expect(locator).toHaveScreenshot() to compare one component. The first run creates a reference image; later runs compare new captures with it. Review and commit the baseline, and run comparisons in a consistent rendering environment.
This guide uses the JavaScript Playwright Test API. The assertion methods are part of the Playwright test runner, not a generic screenshot comparison API for arbitrary runners. Check the documentation for the Playwright version installed in your project, because options and behavior can change.
1. Install Playwright Test and add a visual test
If your project already uses Playwright Test, keep its installed version and configuration. Otherwise, install the test package and browser using the official setup flow:
npm init playwright@latest
Create a test such as tests/visual.spec.js. This complete example visits a page, waits for a meaningful UI state, and captures a focused component:
import { test, expect } from '@playwright/test';
test('pricing card matches its visual baseline', async ({ page }) => {
await page.goto('http://localhost:3000/pricing');
await expect(page.getByRole('heading', { name: 'Choose a plan' })).toBeVisible();
const card = page.getByTestId('pricing-card-pro');
await expect(card).toHaveScreenshot('pricing-card-pro.png');
});
Use a locator for a component whose appearance the test owns. Use a page screenshot when the composition of the whole page is what you want to protect:
test('pricing page matches its visual baseline', async ({ page }) => {
await page.goto('http://localhost:3000/pricing');
await expect(page).toHaveScreenshot('pricing-page.png');
});
Prefer an explicit, descriptive screenshot name where it clarifies what the image represents. Playwright stores expected images alongside the test in snapshot directories. Keep those images in source control so changes to the interface and their reviewed expected appearance travel together.
2. Generate and review the first baseline
- Start the application in a known state, with stable data and the same test account or fixtures each run.
- Run the visual test. If no reference image exists, Playwright creates one.
- Open the generated image and check that it represents the intended UI state. A generated baseline is not automatically evidence that the UI is correct.
- Commit the reviewed reference image with the test.
- Run the test again. Later runs capture a new image and compare it against the committed reference.
The page screenshot assertion waits for two consecutive screenshots to match before it compares them. This settling behavior reduces captures taken during an immediate visual transition, but it cannot make changing application data or other dynamic content deterministic by itself. See the official Visual comparisons guide and PageAssertions API.
3. Control screenshot noise
Visual comparisons can differ even when the application code is unchanged. Playwright’s documentation says browser rendering can vary with the host OS, browser version and settings, hardware, power source, headless mode, and other factors. Generate and compare baselines in a consistent environment; see Playwright Visual comparisons.
Make the application state repeatable
- Use fixed test data and predictable dates, prices, names, and counts.
- Wait for a visible state that matters to the test instead of relying on a guessed delay.
- Disable or stabilize animations when they make captures intermittent. The screenshot assertion supports capture options such as animation control; confirm the option names for your installed version in the PageAssertions API.
- Mask genuinely volatile regions, such as a live clock or rotating advertisement, rather than hiding large areas that could contain regressions.
- Use the documented stylesheet filtering or screenshot capture options where appropriate. Keep any filter narrowly scoped and review it as part of the test.
Do not mask content merely because it is inconvenient to stabilize. If a component is important to the behavior under test, make its data deterministic so visual changes remain visible.
Choose the right comparison scope
| Scope | Use it when | Trade-off |
|---|---|---|
| Locator screenshot | The test owns one component, dialog, menu, or section. | Focused failures are easier to diagnose; page layout outside the component is not checked. |
| Full-page screenshot | The test owns page composition, spacing, and relationships among sections. | More page content can introduce unrelated noise and produce a larger diff. |
Choose one browser and operating environment for stable regression detection. Add a browser or OS matrix when cross-browser rendering is part of the coverage goal; keep each environment’s baselines consistent with the environment that generated them.
4. Set a comparison tolerance
Start with strict defaults. If a failure shows small rendering noise that the team has reviewed and decided is acceptable, use the narrowest tolerance that addresses that noise. Screenshot assertion options include maxDiffPixels, maxDiffPixelRatio, and a color threshold. Options can be passed to an assertion or configured for common expectations; see the SnapshotAssertions API and TestConfig.
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 50,
});
That value is an example of syntax, not a recommended universal tolerance. A pixel count is easier to reason about on a fixed-size component; a ratio scales with image size. The color threshold changes how much color difference a pixel comparison accepts. Increasing tolerance can hide real regressions, so inspect the diff before changing it and keep the accepted difference as small as practical.
To establish a shared policy, configure an expectation in playwright.config.js:
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 50,
},
},
});
Use global settings only when they make sense across the suite. A broad global tolerance may weaken checks for small components; a per-test value can document why one particular capture needs it.
5. Update baselines for intentional changes
When a design change is intentional, update snapshots with Playwright’s documented workflow:
npx playwright test --update-snapshots
- Run the update in the same environment used for the project’s baselines.
- Review each changed expected image. Confirm the new appearance matches the intended UI change.
- Review the test and application diff together; do not accept unrelated baseline changes automatically.
- Commit the reviewed images with the code change.
The update command replaces expected results with current captures. It does not decide whether the change is desirable.
6. Diagnose a visual test failure
Start by comparing the expected image, the actual capture, and the diff. Determine whether the mismatch is a real UI regression, unstable page state, or an environment change. Use Playwright’s Trace Viewer to inspect the recorded actions and page state around the capture.
| Symptom | Likely cause | What to do |
|---|---|---|
| Many unrelated snapshots fail on one machine | Browser, OS, rendering settings, hardware, or headless environment differs from the baseline environment. | Run comparisons in the baseline environment, or deliberately establish and review baselines for the new environment. |
| Only a changing label, timestamp, or image differs | Live data, time-dependent content, or rotating content is not stable. | Fix the test data or time, wait for the desired state, or narrowly mask the truly volatile region. |
| The screenshot captures a transition | The UI animation or app update is still in progress. | Wait for a meaningful state; use the supported animation capture option if appropriate. Remember that screenshot settling alone cannot stabilize app data. |
| The first run has no comparison result | No expected snapshot existed, so the test generated a baseline. | Inspect the generated file and commit it; run again to perform a comparison. |
| A legitimate change causes a mismatch | The expected image still represents the old UI. | Review the visual change, update snapshots with --update-snapshots, inspect the resulting images, and commit them. |
| A test passes despite a visible difference | Tolerance is too broad, or a mask/filter hides the changed area. | Inspect the options and reduce tolerance or narrow the mask/filter. |
| A full-page diff is difficult to interpret | The capture includes unrelated dynamic sections or too much page content. | Stabilize data or move a component-specific assertion to a locator screenshot. |
7. Performance, reliability, and cost
Playwright screenshot comparisons run as part of your browser tests, so their runtime includes browser startup, navigation, app readiness, image capture, and comparison. Keep tests focused, reuse the project’s normal browser setup, and avoid capturing large pages when a component-level assertion answers the question. The research sources do not provide a general runtime benchmark, so measure your own suite before changing parallelism or splitting projects.
Reliability depends on repeatable rendering conditions and reviewed baselines. Cross-machine rendering differences are expected factors to manage, not a reason to accept broad tolerance everywhere. Repository-managed baselines have no per-capture API charge; the practical costs are test runtime, CI capacity, and reviewing snapshot changes. No universal cost or speed estimate applies across CI providers and application sizes.
Or skip the browser setup
If you need a screenshot artifact outside a Playwright assertion, ScreenshotNeo is a website screenshot API and MCP server. Its API takes one GET request with a URL and returns PNG, JPEG, WebP, or PDF. This does not replace Playwright’s repository-managed visual assertions: it is a simpler way to capture a page without setting up browser automation.
API docs: ScreenshotNeo documentation.
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}`);
await Bun.write('shot.webp', res);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its 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. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Can I use toHaveScreenshot() with another test runner?
The documented screenshot assertion APIs are for Playwright Test. If your project uses another runner, use its own assertion workflow or run visual checks in Playwright Test.
Should every test get a screenshot assertion?
No. Add one where the appearance is an important contract of the page or component. Keep behavior checks in their usual assertions so a visual diff does not have to explain every failure.
Why should baselines be reviewed by a person?
A baseline records what was rendered. It cannot tell whether that rendering is correct or whether a change was intended.


