Playwright Screenshots with Chromium vs Firefox vs WebKit
Run Playwright visual tests in Chromium, Firefox, and WebKit, understand why screenshots differ, and manage browser-specific baselines in CI.
Short answer: Use Playwright projects to run the same screenshot test in Chromium, Firefox, and WebKit. Expect the images to differ: rendering depends on the browser build, operating system, browser settings, hardware, headless mode, and screenshot scale. Generate and review visual baselines in a controlled environment, and keep separate baselines for browser or platform variants when those differences matter to your users.
Playwright’s Firefox is a patched build, and Playwright WebKit is built from WebKit main-branch sources rather than branded Safari. For the closest Safari experience, Playwright recommends running WebKit on macOS. See the official browser documentation.
1. Configure Chromium, Firefox, and WebKit projects
Projects let one test suite run with separate browser configurations. Add three projects to playwright.config.ts and install the corresponding Playwright-managed browsers.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
reporter: 'list',
use: {
baseURL: 'http://127.0.0.1:3000',
headless: true,
viewport: { width: 1440, height: 900 },
// Keep screenshot output to one pixel per CSS pixel.
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Install Playwright Test and its managed browsers from the project directory:
npm install --save-dev @playwright/test
npx playwright install chromium firefox webkit
The device descriptors set a browser-appropriate user agent and viewport defaults. Here, the shared use values set the viewport for all projects. If you need to compare the same exact geometry, set the viewport explicitly and avoid accidental per-project differences. Playwright’s projects guide covers project configuration and selecting projects.
Run the suite or target a single engine
# Run all configured projects
npx playwright test
# Run only one browser project
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit
# Run one test file in WebKit
npx playwright test tests/homepage.spec.ts --project=webkit
Project names are configuration labels; they do not change the browser engine. Use clear names if you later add platform or device variants, such as webkit-macos or chromium-mobile.
2. Write a screenshot assertion
Use toHaveScreenshot() for a visual regression assertion. It captures repeatedly until two consecutive screenshots match, then compares the stabilized image with the stored reference. A first run may create reference images; review them before accepting them as the expected output.
import { test, expect } from '@playwright/test';
test('homepage visual appearance', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
animations: 'disabled',
});
});
Run the test once to create missing references, inspect the generated images, then rerun to compare. To intentionally refresh references after reviewing an approved UI change:
npx playwright test --update-snapshots
Do not accept every generated diff automatically: a changed reference can hide a real regression. Playwright documents stabilization, reference naming, and comparison behavior in Visual comparisons.
3. Why do screenshots differ between Firefox and Chromium?
A screenshot is the output of a rendering environment, not just the HTML and CSS. Playwright lists host operating system, browser version, settings, hardware, power source, and headless mode as factors that can affect rendering. Differences can therefore appear even when the application state and test code are identical.
| Factor | How it can affect a capture | How to control it |
|---|---|---|
| Browser engine and build | Engines can render fonts, layout, and other page details differently. Playwright-managed builds also differ from branded browser releases. | Pin the Playwright version and install its browsers; use a baseline for each project that matters. |
| Operating system and machine | Font availability, rendering environment, and machine characteristics can change pixels. | Generate and compare references on the same OS image and CI environment. |
| Headless or headed mode | Rendering can vary with execution mode. | Use the same mode when creating and checking references. |
| Viewport and device scale | Different viewport dimensions change responsive layout; device pixel scale changes output dimensions and rasterization. | Set viewport and screenshot scale explicitly. |
| Animation and dynamic content | Moving elements, timestamps, rotating content, and live data can change between captures. | Disable or mask motion and stabilize application data before asserting. |
| Assets and fonts | A capture taken before fonts or images are ready may differ from a later capture. | Wait for the application’s readiness condition and required assets. |
Do you need separate baselines for each browser? Usually, yes, if the goal is to detect regressions within each engine: Chromium’s expected image should be compared with Chromium’s output, Firefox with Firefox, and WebKit with WebKit. Playwright snapshot names can include browser and platform, and projects can contribute their names. Treat references as versioned project artifacts and review updates alongside the change that caused them.
For the reasoning behind environment control and browser-specific snapshots, see Playwright’s visual comparison guidance.
4. Control capture geometry and pixel scale
Choose whether the test captures only the viewport or the entire scrollable page. The Page screenshot API supports both. fullPage: true captures the full page; omitting it captures the viewport. Playwright’s Page API describes these options.
// Viewport screenshot
await page.screenshot({ path: 'viewport.png' });
// Full scrollable page
await page.screenshot({ path: 'full-page.png', fullPage: true });
// One output pixel per CSS pixel
await page.screenshot({ path: 'css-scale.png', scale: 'css' });
// One output pixel per device pixel
await page.screenshot({ path: 'device-scale.png', scale: 'device' });
CSS scale produces one image pixel per CSS pixel. Device scale produces one pixel per device pixel and can create larger high-DPI images. For stable cross-browser comparisons, explicitly choose a scale and keep it fixed. Also keep the viewport width and height the same: responsive breakpoints can make a small width difference become a large layout difference.
Capture a specific region
For a component-level assertion, pass a locator to toHaveScreenshot() instead of comparing the entire page:
test('navigation renders as expected', async ({ page }) => {
await page.goto('/');
const navigation = page.getByRole('navigation');
await expect(navigation).toHaveScreenshot('navigation.png', {
animations: 'disabled',
});
});
Keep the target stable and uniquely identifiable. A component capture reduces unrelated page changes in the comparison, but it will not catch regressions outside that component.
5. Stabilize screenshots without hiding real regressions
- Disable animation deliberately. Screenshot assertions default to disabling animations; the Page screenshot API has different behavior and leaves animations untouched by default. Set the policy explicitly when comparing calls or mixing APIs. See the PageAssertions API and Page API.
- Wait for meaningful readiness. Prefer an application-specific ready marker or a relevant locator over an arbitrary long sleep. If a page depends on remote content, make the test data deterministic.
- Mask volatile regions. Mask timestamps, rotating avatars, or other values that are expected to vary. Do not mask areas where changes would indicate a defect.
- Use screenshot-only styles sparingly. A stylesheet override can hide a transient element or normalize a dynamic area. Keep the override narrow and documented so it does not conceal a layout issue.
- Keep fonts and assets available. Use the same local or controlled assets in baseline generation and comparison. A missing font can change line breaks and shift surrounding content.
- Stabilize application state. Fix the test account, data, locale, timezone, and feature flags when they affect rendered output.
Example with a masked changing value:
test('account page visual appearance', async ({ page }) => {
await page.goto('/account');
await page.getByRole('heading', { name: 'Account' }).waitFor();
await expect(page).toHaveScreenshot('account.png', {
fullPage: true,
animations: 'disabled',
mask: [page.locator('[data-testid="last-updated"]')],
});
});
6. Choose image-difference tolerances carefully
Start with strict comparisons. If a known rendering difference creates noise, allow a small, documented tolerance scoped to the specific assertion or project. Playwright supports a per-pixel threshold, a maximum number of different pixels through maxDiffPixels, and a maximum differing-pixel ratio through maxDiffPixelRatio.
await expect(page).toHaveScreenshot('chart.png', {
animations: 'disabled',
// Example values only: calibrate against your own page and environment.
threshold: 0.15,
maxDiffPixels: 25,
});
The values above are examples, not universal recommendations. A permissive threshold can conceal meaningful changes. Record why a tolerance exists, keep it as narrow as practical, and review diffs visually. See the official PageAssertions options for supported comparison controls.
7. Keep CI and baseline generation consistent
- Pin the Playwright package version in the lockfile.
- Install browsers using that package’s Playwright command, rather than relying on an arbitrary system browser.
- Generate references in the same operating system image, browser build, viewport, scale, and headless mode used for comparisons.
- Commit the reviewed references with the code. Treat them as test artifacts that need code review when they change.
- Run each project in CI. If a failure occurs, rerun the failing project in the same environment before changing a baseline.
- Use WebKit on macOS when the goal is the closest Playwright-provided Safari experience; label other WebKit runs accurately.
Playwright’s guidance is to use the same environment in which the baselines were generated. A developer laptop and a Linux CI runner can produce different screenshots even when both run the same test suite. For browser build details and platform considerations, consult Browsers.
8. Performance, reliability, and maintenance trade-offs
Each project runs tests against another browser configuration, so adding projects increases total browser work. Parallel execution can reduce wall-clock time, while machine capacity, browser startup, and page complexity affect how much parallelism is useful. Avoid raising parallelism without checking for resource contention: overloaded workers can make timing-sensitive tests less reliable.
Full-page images contain more pixels than viewport captures and can take more time and storage to generate and review. Prefer a component or viewport assertion when it covers the regression risk; keep full-page assertions for flows where the whole layout matters. High device scale also increases image dimensions. The main maintenance cost is reviewing and updating references for every browser or platform combination you decide to support. Add a variant when it represents meaningful user coverage, not just to maximize the matrix.
Playwright does not make captures identical across environments. Reliability comes from controlling the environment, application state, geometry, and comparison policy, then investigating unexpected differences instead of broadly loosening tolerances.
9. Troubleshooting common screenshot failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Chromium passes, Firefox or WebKit fails | Engine-specific rendering or behavior, or a real cross-browser layout issue. | Run only the failing project, inspect its actual image, and decide whether the difference is intended. Fix the page if it is a product defect; maintain a separate reviewed reference if the engine output is expected. |
| Local passes but CI fails | Different OS, browser build, headless mode, fonts, or machine environment. | Generate and compare baselines in the same CI image and mode. Check the Playwright version and installed browser versions. |
| Screenshot changes between runs | Animation, dynamic content, time-dependent state, unstable data, or assets not ready. | Disable animations, mask only genuinely volatile elements, fix test data, and wait for an application readiness condition. |
| Image is unexpectedly large or blurry | Device pixel scale, viewport, or device descriptor differs. | Set viewport and scale explicitly; use scale: 'css' when one image pixel per CSS pixel is desired. |
| Text wraps differently | Different font availability or loading state, viewport width, or engine text rendering. | Use consistent fonts and wait for readiness; compare the same geometry and keep per-engine references where appropriate. |
| Every test fails after a Playwright upgrade | Browser builds or rendering output may have changed with the toolchain. | Inspect diffs, verify the package and browser versions, then update only intentional reviewed references. |
| Snapshot cannot be found | Reference was not generated for the project/platform or snapshot naming differs. | Check the project name, snapshot path and naming configuration, then generate references for the intended project and review them. |
| Baseline update hides a regression | References were updated without inspecting the resulting images. | Review every changed image and the application diff before accepting the update. |
10. Or skip the browser setup
If you need an image of a public page rather than an in-test browser baseline, ScreenshotNeo provides a one-request screenshot API and an MCP server. It does not replace Playwright’s browser-specific visual assertions, but it can avoid managing browser capture infrastructure for page captures.
See the ScreenshotNeo API documentation for request 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
11. FAQ
Does Playwright WebKit mean I am testing Safari?
It tests Playwright’s WebKit build, not branded Safari. Playwright identifies WebKit on macOS as the closest Safari experience it provides.
Can one expected screenshot be shared across all three projects?
It is possible to configure shared naming, but it is usually a poor fit when each engine renders differently. Separate references let each project detect changes against its own expected output.
Should I use full-page screenshots for every test?
No. Use full-page capture when changes anywhere on the page matter. Component and viewport captures are smaller and can make failures easier to diagnose.
Does disabling animation make the whole page deterministic?
No. It addresses animation behavior, but dynamic data, fonts, asset readiness, environment differences, and other changing state still need attention.


