Use Playwright Test’s toHaveScreenshot() matcher: call it on page to compare an entire route, or on a locator to compare one component or region. The first run writes a reviewed baseline image; later runs capture the page, wait for two consecutive identical frames, and compare the result with that baseline using pixelmatch.
Set up a visual comparison test
Install Playwright Test in your project, create a test file, and run it once to generate the expected image. This example uses TypeScript and captures a complete page:
import { test, expect } from '@playwright/test';
test('homepage visual baseline', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
animations: 'disabled',
mask: [page.locator('[data-testid="live-clock"]')],
threshold: 0.2,
maxDiffPixels: 100,
});
});
Run the test with your normal Playwright command, such as npx playwright test. On the first execution, Playwright creates the snapshot in the test’s snapshot directory. Open that image, confirm it represents the intended UI, and commit it to version control alongside the test. A later run fails when the rendered image exceeds the configured difference policy.
What the matcher does before comparing
Playwright does not immediately compare the first frame it sees. Its page-assertion API waits until two consecutive page screenshots are identical, then compares the last one with the expectation. This reduces failures caused by a layout settling over a few frames, but it cannot make nondeterministic application data deterministic.
Free tools Windows power users keep installed
One-click scans. No signup required.
Keep baselines reviewable
Store snapshots in the Playwright Test snapshot directory and review image diffs in code review. Generate and verify baselines with the same browser project, operating-system image, viewport, device scale factor, fonts, locale, timezone, and test data used by CI. If a design change is intentional, update the baseline in that same change and have a person review the new image.
Choose page or component scope
Compare a full page
Use a page assertion when the visual contract includes navigation, page layout, responsive composition, or the complete route:
test('account page', async ({ page }) => {
await page.goto('/account');
await expect(page).toHaveScreenshot('account.png', { fullPage: true });
});
fullPage: true captures the full scrollable document rather than only the current viewport. Full-page assertions are useful for route-level regressions, but they also include more content that can change for reasons unrelated to the feature under test.
Compare one locator
Use a locator assertion when a component is the contract and the surrounding page would add noise. Cards, dialogs, tables, charts, and controls are common candidates:
test('checkout summary card', async ({ page }) => {
await page.goto('/checkout');
const summary = page.getByTestId('order-summary');
await expect(summary).toHaveScreenshot('order-summary.png');
});
Locator screenshots make failures easier to interpret and let unrelated page changes proceed without rewriting a component’s baseline. Make the locator specific enough that it resolves to the intended element; a broad or unstable selector can capture the wrong node.
Make screenshots deterministic
Animations and transitions
Animations are disabled by default for screenshot assertions. Finite animations are fast-forwarded to completion; infinite animations are canceled to their initial state during capture and resumed afterward. Keep the default unless motion itself is the behavior under test. Set animations: 'allow' only when you deliberately want to compare motion-related rendering.
Mask changing regions
Mask timestamps, rotating promotions, avatars, ads, live counters, and other pixels outside your visual contract:
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.getByTestId('last-updated'),
page.locator('.rotating-promotion'),
],
maskColor: '#FF00FF',
});
maskColor controls the replacement color, making masked areas obvious in a diff. Masking also covers invisible elements unless visibility filtering is configured separately, so scope masks carefully and avoid masking a container that contains pixels you do need to test.
Apply a shared style override
Use stylePath to inject a stylesheet while capturing. It is useful when many tests must hide carets, transitions, or known dynamic selectors:
await expect(page).toHaveScreenshot('editor.png', {
stylePath: 'tests/visual-stability.css',
});
For example, the stylesheet can disable a blinking caret or hide a known live region. Keep such overrides narrowly targeted; a broad rule can conceal a real regression.
Control data and readiness yourself
The two-identical-frame wait is not a substitute for deterministic test setup. Freeze clocks where time appears in the UI, mock changing API responses, wait for required content and fonts, and remove random identifiers from rendered markup. Prefer a stable fixture or seeded data set over masking an entire data table. If a page loads content after the screenshot settles, explicitly await the relevant locator or application-ready signal.
Set a sensible diff policy
Playwright Test uses pixelmatch for image comparison. Three options control how much difference is accepted:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minute| Option | Meaning | How to use it |
|---|---|---|
threshold |
Per-pixel perceived color-difference tolerance from 0 (strict) to 1 (lax); pixelmatch computes color difference in YIQ space. |
Start strict and increase only after identifying measured rendering noise. |
maxDiffPixels |
Absolute cap on changed pixels. | Allow a known, small number of antialiasing changes without accepting a broad layout shift. |
maxDiffPixelRatio |
Cap on the fraction of changed pixels. | Useful when image dimensions vary by project or viewport. |
For example:
await expect(page).toHaveScreenshot('pricing.png', {
threshold: 0.1,
maxDiffPixels: 250,
maxDiffPixelRatio: 0.005,
});
A high threshold or generous pixel budget can turn a meaningful regression into a passing test. Tune one value at a time, inspect the actual diff, and document why the allowance exists.
Review a failure instead of blindly updating
- Open the actual image, expected baseline, and diff image produced by the test runner.
- Classify the change as a real UI regression, an intentional design update, or nondeterministic content.
- If it is nondeterministic, fix its source: mock the response, freeze the clock, wait for fonts or data, or mask only the pixels outside the contract.
- Check that the browser project and rendering environment match the baseline environment.
- Update the snapshot only after a human reviews the visual diff and confirms the change is intentional.
Do not treat snapshot regeneration as a repair command. It replaces the reference and can permanently encode a broken layout if the failure was not investigated.
Compare arbitrary screenshot buffers
Playwright also supports expect(await page.screenshot()).toMatchSnapshot('landing-page.png'). That matcher is appropriate for arbitrary buffers or non-page snapshot data. For page screenshot comparison, Playwright’s SnapshotAssertions guidance recommends toHaveScreenshot(), because it provides the page/locator screenshot behavior and stabilization described above.
Rank #4
Common problems and fixes
The first run fails or has no baseline
The first execution should create the expected snapshot. Check that the test has permission to write to its snapshot directory and that you ran the intended project. Review the generated image before committing it.
Recommended Free Tools
Every run differs by a few pixels
Look for timestamps, animated content, random IDs, rotating ads, caret blinking, font loading, or platform-specific antialiasing. Freeze or mock the source, wait for fonts, use a focused mask, or apply a targeted stylePath. Only then consider a small threshold or pixel budget.
The diff is a large vertical shift
This usually indicates missing content, a late-loading font, a responsive viewport mismatch, or a different browser/OS image—not harmless noise. Verify readiness waits and environment parity before changing tolerances.
A masked area still produces surprising results
Ensure the locator resolves to the intended element and remember that masking can include invisible elements. Narrow the locator, use visibility filtering where appropriate, and avoid masking a parent that contains tested content.
CI fails while local runs pass
Use the same Playwright browser version, OS image, fonts, viewport, device scale factor, locale, timezone, and fixture data in both places. Baselines generated on one rendering environment are not automatically portable to another.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Best Value
A deliberate redesign fails hundreds of tests
Review the diffs in batches, update only the affected snapshots, and commit the baseline changes with the UI change. Keep unrelated failures visible instead of regenerating every snapshot indiscriminately.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If you need a rendered image or PDF outside a test suite, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status.
One GET request is enough. See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also offers full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets plus custom viewports, retina scale, PNG/JPEG/WebP or PDF output, custom CSS and JavaScript, clicks before capture, waits for selectors, delays or network idle, request and resource blocking, custom headers/cookies/user agents/Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs are accepted to ease migration.
For AI workflows, its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Plans include 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.
Performance, reliability, and cost decisions
- Keep scope intentional: locator tests are generally easier to diagnose and contain less unrelated markup than full-page captures.
- Reuse stable fixtures: deterministic data prevents reruns and expensive debugging caused by false positives.
- Parallelize safely: run independent pages in parallel, but avoid shared mutable test data that changes pixels between workers.
- Cache deliberately: for external captures, a chosen cache TTL can reduce repeat work; in visual tests, ensure cached content is the content you intend to verify.
- Budget review time: every baseline is a versioned artifact that needs human review when it changes; a larger snapshot suite increases that review surface.
Practical decision checklist
- Is the contract the whole route or one component?
- Are browser, OS, fonts, viewport, scale factor, locale, timezone, and data fixed?
- Have clocks, API responses, fonts, and readiness signals been controlled?
- Are masks limited to pixels genuinely outside the contract?
- Are
threshold,maxDiffPixels, ormaxDiffPixelRatiojustified by observed noise? - Will a reviewer inspect and commit intentional baseline changes?
Frequently Asked Questions
Can I compare screenshots from different browsers?
Yes, but keep separate baselines when rendering differences are expected. A baseline is most reliable when generated and checked with the same browser project and operating-system image.
Should visual tests run on every pull request?
Run them on pull requests when the rendering environment is stable, then retain a consistent CI job for the protected branch so approved changes remain covered.
How do I test an animation itself?
Set animations: 'allow' deliberately and design the assertion around the motion state you intend to verify; the default behavior disables animations for stable screenshots.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

