How to Compare Website Screenshots Across Browsers with Playwright
Compare Chromium, Firefox, and WebKit screenshots with Playwright. Set browser-specific baselines, control rendering noise, and investigate visual diffs.
Use expect(page).toHaveScreenshot() in Playwright Test and run the same tests in Chromium, Firefox, and WebKit projects. Playwright creates a reference screenshot on the first run, then compares later captures against it. Keep baseline generation and comparison on the same operating system, browser versions, viewport, and settings; browser-specific baselines are normal because engines and environments can render pages differently.
This guide sets up a runnable cross-browser visual comparison, explains baseline updates and diff controls, and shows how to diagnose failures without masking real regressions.
1. Create browser projects
Projects let one test suite run against multiple browser configurations. The configuration below uses Playwright-managed Chromium, Firefox, and WebKit. Each project gets its own screenshot snapshots, so the test compares a run with the matching engine’s reference.
Install Playwright Test
npm init playwright@latest
If prompted, choose JavaScript or TypeScript and install the browser binaries. In an existing project, install the test package and its browsers using the commands in the Playwright installation guide. Keep the Playwright package and browser versions consistent with the environment that owns your baselines.
Configure projects
Put this in playwright.config.ts. If your project uses JavaScript, save it as playwright.config.js and remove the TypeScript type annotations if present in your surrounding setup.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{arg}{ext}',
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], browserName: 'chromium' },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'], browserName: 'firefox' },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'], browserName: 'webkit' },
},
],
});
The explicit snapshot path includes the project name, making the engine-specific reference files easy to locate. Playwright also supports other snapshot naming patterns; see Visual comparisons and Projects for current configuration details.
2. Write a visual comparison test
Create tests/homepage.spec.ts. Replace the example URL and any page-specific readiness condition with your application’s route and a stable signal that its important content has rendered.
import { test, expect } from '@playwright/test';
test('homepage visual appearance', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Prefer a meaningful readiness condition when the page has one.
await expect(page.locator('h1')).toBeVisible();
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
});
});
Run all configured projects with:
npx playwright test
On its first run, the assertion writes a reference image. Subsequent runs capture repeatedly until two consecutive screenshots match, then compare the result to that reference. Review and commit accepted reference images with the test change so CI and other developers have the same expectation.
3. Generate and update baselines deliberately
- Run the visual tests in the environment you intend to use for both baseline creation and ongoing comparison.
- Inspect the generated screenshots to confirm they show the intended page state, viewport, and browser project.
- Commit the reference images alongside the test.
- When an intentional design change alters appearance, inspect the new captures and update references with
npx playwright test --update-snapshots. - Review the snapshot diff in version control before accepting it. An update command changes expectations; it does not establish that the page is correct.
Do not regenerate snapshots as a routine response to every failure. First determine whether the difference is a product regression, an intentional change, or environment drift.
4. Choose browser, channel, and device coverage
Coverage should match the browsers and viewports your application supports. The example above compares three engines at a desktop viewport. You can add projects for branded Chrome or Edge channels and for emulated mobile configurations. Playwright projects run as part of the suite unless you select a subset; available workers and configuration affect how much runs at once.
| Comparison axis | What to decide | Practical guidance |
|---|---|---|
| Browser engine | Chromium, Firefox, WebKit | Include engines relevant to your audience and support promise. Use separate project baselines. |
| Browser distribution | Playwright-managed engine or branded Chrome/Edge channel | Test a branded channel when that distribution is part of the behavior you need to support. |
| Viewport and device | Desktop sizes and emulated mobile configurations | Add sizes where responsive layout changes or where users rely on the interface. |
| Baseline environment | Operating system, browser version, rendering settings | Keep them controlled between baseline creation and comparison. |
| Capture scope | Full page or a focused element | Use full-page captures for page-level regressions; focused captures can make a volatile or very large page easier to inspect. |
For example, add a mobile project using a device descriptor:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'chromium-desktop', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit-mobile', use: { ...devices['iPhone 13'], browserName: 'webkit' } },
],
});
Device descriptors set a bundle of emulation properties. If your comparison requires an exact viewport or other explicit context, set those values in the project or test and keep them the same for baseline and comparison runs.
5. Control rendering noise without hiding defects
Visual output can change with the host operating system, browser version, browser settings, hardware, power source, and headless mode. Fonts are a common source of differences. Playwright’s guidance is to keep the operating system and browser versions the same for visual regression tests. A baseline from one platform may not be a meaningful pixel-level reference for another.
Set a justified pixel tolerance
For small, understood rendering variation, allow a limited number of different pixels with maxDiffPixels, or adjust the perceived-color threshold. Keep tolerances narrow and document why they are needed.
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 80,
threshold: 0.15,
});
These options are version-sensitive. Check the PageAssertions API for the installed Playwright version. A permissive threshold can conceal a real color, spacing, or content regression.
Mask or hide only known volatile content
For content that is intentionally dynamic, use a mask for the changing region or a screenshot stylePath stylesheet to suppress a known unstable element. Keep the rest of the page visible to comparison. Do not mask broad containers just to make failures pass.
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
mask: [page.locator('[data-visual-volatile]')],
});
Use selectors that identify only the changing content. If an animation or delayed layout is causing instability, prefer making that state deterministic in the test or application fixture rather than applying a large difference allowance.
6. Debug a failed screenshot comparison
- Open the diff. Compare expected, actual, and diff images. Identify whether the change is localized, page-wide, or limited to text and fonts.
- Check the project and metadata. Confirm the browser project, viewport, operating system, browser version, and headless setting match the baseline context.
- Check page readiness. Make sure the capture happens after the content under test is visible and the relevant layout has settled.
- Reproduce in the baseline environment. A failure that only appears on a different host or browser version may be environment drift; reproduce before changing the reference.
- Decide whether the change is intentional. Fix a regression in the page. Update snapshots only after reviewing an intentional design change.
The Playwright Trace Viewer provides expected, actual, and diff images, a slider comparison, and browser and viewport context. Enable and inspect traces for the failing run using the Trace Viewer documentation.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshot is missing on first run | No reference has been created yet, or the process cannot write to the snapshot directory. | Run the test in a writable environment, inspect the generated snapshot path, and commit the reference. |
| Many unrelated tests fail after a dependency update | Browser binaries or rendering environment changed while the baselines stayed the same. | Restore the baseline environment or intentionally regenerate and review snapshots in the new controlled environment. |
| Only one browser project fails | Engine-specific rendering or behavior, or a project-specific configuration difference. | Inspect that project’s actual image and metadata; fix browser-specific application behavior if needed. Keep its baseline separate. |
| Text looks different everywhere | Font availability, font loading, operating system, or browser version differs. | Use a consistent host and browser version, and wait for a meaningful page-ready condition that includes the required font/content state. |
| Intermittent diffs move between runs | Animations, timestamps, randomized content, delayed widgets, or other unstable page state. | Make test data deterministic, wait for the relevant state, or narrowly mask/hide the known volatile element. |
| Full-page image has unexpected blank or shifted areas | Lazy-loaded content has not appeared, or page layout changed while capture was settling. | Scroll or otherwise trigger the content as part of setup, wait for it to appear, and verify the page’s final layout before capture. |
| Raising the tolerance makes tests pass but misses changes | The threshold is broad enough to ignore meaningful visual changes. | Reduce tolerance and address the known source of noise directly; review the expected/actual/diff images. |
| Snapshot updates produce a large diff | Baseline environment, viewport, fonts, browser version, or page state differs from the prior run. | Check environment and test setup before accepting the update. Regenerate only when the new reference is intentional. |
8. Performance, reliability, and cost
Each configured project runs the tests against its browser configuration, so adding engines and device projects increases total browser work. Use the projects that match your support requirements, and use Playwright’s worker settings to fit available CI capacity. Large full-page captures and broad suites also produce more snapshot files to review and store.
Reliability depends on controlling the inputs to rendering: browser and operating system versions, viewport, fonts, page data, and capture readiness. Keep those inputs explicit, and treat a baseline as an expectation for that configuration rather than a universal image for every platform. Review snapshot changes as code changes.
Playwright is an open-source testing framework; this workflow has no per-screenshot API charge described by the cited guidance. Budget for the compute and CI time required to run each browser project, plus maintenance of browser binaries and reference images. No performance benchmark or fixed runtime is implied here.
Or skip the browser setup
If you need a clean capture of a public page without installing browser binaries or maintaining screenshot infrastructure, ScreenshotNeo is a website screenshot API and MCP server. It does not replace Playwright’s browser-specific regression assertions, but it can return a page screenshot with one GET request. See the ScreenshotNeo API 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,
)
r.raise_for_status()
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, and response headers say the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Should I use one baseline for Chromium, Firefox, and WebKit?
No. Use the baseline associated with each project and engine. Rendering can differ across engines and platforms.
Does a passing screenshot test prove the page looks identical on every computer?
No. It shows that the capture matches its reference under the tested configuration. Other operating systems, browser versions, fonts, and settings can render differently.
When should I update snapshots?
After confirming a visual change is intentional and reviewing the new images. Snapshot updates change the expected result; they do not diagnose the cause of a diff.
Can I compare only one component?
Yes. Playwright supports screenshot assertions on locators as well as on the page. A focused element capture can keep unrelated page content out of a component’s visual check.


