ScreenshotNeo

BlogHow-to

How to Make Consistent Website Screenshots Across Chrome and Firefox

Control browser, viewport, scale, page state, and capture target to make Chrome and Firefox screenshots repeatable and comparisons meaningful.

By the ScreenshotNeo team4 October 202611 min read

To make website screenshots consistent across Chrome and Firefox, control the capture conditions and compare each browser with its own reference image. Chrome and Firefox use different rendering engines, so matching settings does not guarantee pixel-identical output. For visual regression testing, keep separate Chrome and Firefox baselines; compare the browsers directly only when you want to investigate cross-browser differences.

Playwright warns that rendering can vary with the host operating system, browser version, settings, hardware, power source, and headless mode. Its guidance is to use the same environment for screenshots and their baselines. Playwright: Visual comparisons.

1. Decide what “consistent” means

There are two useful, distinct goals:

  • Regression detection: Did Chrome change since its last accepted Chrome capture? Compare Chrome against a Chrome baseline. Repeat independently for Firefox.
  • Cross-browser investigation: How does the page differ between Chrome and Firefox under matched conditions? Capture both and inspect the differences as browser-specific behavior; do not assume every pixel should match.

Playwright snapshot names include browser and platform identity, reflecting that screenshots can differ across browsers and platforms because of rendering, fonts, and other factors. See the snapshot documentation.

2. Set up a reproducible Playwright project

The following example uses Playwright Test to take visual snapshots in Chromium and Firefox. It pins both the viewport and device scale factor, waits for the page to reach a chosen state, and uses the same test in both browser projects.

npm init -y
npm install --save-dev @playwright/test
npx playwright install chromium firefox

Create playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  use: {
    baseURL: 'http://127.0.0.1:3000',
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 1,
    colorScheme: 'light',
    locale: 'en-US',
    timezoneId: 'UTC',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
  ],
});

Create tests/homepage.spec.ts:

import { test, expect } from '@playwright/test';

test('homepage visual appearance', async ({ page }) => {
  await page.goto('/');

  // Wait for application-specific content instead of relying on an arbitrary delay.
  await page.getByRole('heading', { name: 'Welcome' }).waitFor();
  await page.evaluate(() => document.fonts.ready);

  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    animations: 'disabled',
    caret: 'hide',
    style: `
      *, *::before, *::after {
        animation-duration: 0s !important;
        transition-duration: 0s !important;
      }
      [data-visual-test="dynamic"] { visibility: hidden !important; }
    `,
  });
});

Run both projects with npx playwright test. On the first run, Playwright creates reference screenshots. Review them before accepting them as baselines. Later runs compare captures against those references. Run the same command in the same pinned environment when updating or checking those references. Playwright documents baseline creation and updates.

The heading and dynamic-element selector are examples: replace them with elements and test hooks from your app. The test assumes your application is already running at the configured base URL.

3. Match the capture inputs

Input How to keep it consistent Why it matters
Browser and version Run the same Playwright version and browser build for baseline creation and comparison; keep separate browser projects and references. Browser updates can change rendering. A Chrome reference is not a Firefox reference.
Operating environment Use the same operating system and reproducible CI image for both baseline generation and later checks. Keep the image stable while comparing. Fonts, rasterization, hardware, power state, and headless behavior can affect pixels. Pinning is practical operational guidance, not a requirement for a particular CI vendor.
Viewport Set explicit width and height in CSS pixels in the browser context. Responsive breakpoints and line wrapping change with viewport geometry.
Device scale factor and output scale Set a fixed deviceScaleFactor; choose screenshot scale deliberately. CSS-scale images use one output pixel per CSS pixel. Device-scale images use one output pixel per device pixel and can be larger on high-DPI settings. See Playwright screenshot options.
Capture target Use the same viewport, element, or full-page mode in both runs. Different regions or page lengths cannot be compared meaningfully.
Application state Use the same test data, account state, locale, timezone, consent state, and route. Different content or state looks like a rendering change.
Page readiness Wait for a meaningful app signal, fonts, and required images/data. A capture made while content is still loading is unstable.

4. Choose screenshot geometry and scale

Playwright’s page screenshot API supports viewport, full-page, and element captures, plus scale and other screenshot controls. See the API options.

  • Viewport capture: Use for above-the-fold or fixed-viewport checks. It captures what is visible in the viewport.
  • Element capture: Use for a component or region. Locate the same stable element in each browser, then call locator.screenshot(). Avoid selectors whose meaning or position differs across browser-specific markup.
  • Full-page capture: Use when below-the-fold layout matters. It can be taller and more expensive to store and diff. Lazy-loaded content may need explicit scrolling or application-specific loading before the capture.

For example, to capture a single component instead of the whole page:

const card = page.getByTestId('pricing-card');
await expect(card).toHaveScreenshot('pricing-card.png', {
  animations: 'disabled',
  caret: 'hide',
});

Use CSS scale when the question is layout in CSS pixels. Use device scale when output resolution at the emulated device density matters. Do not compare an image captured at CSS scale to one captured at device scale: their dimensions and pixel mapping differ. Playwright documents the scale choices.

5. Make page state stable before capture

Screenshot consistency depends on content as well as browser settings. Navigate to the same URL, establish the same application state, and wait for a meaningful readiness condition. Prefer waiting for a visible heading, a test-specific ready marker, or the completion of a known app operation over sleeping for an arbitrary number of seconds.

await page.goto('/dashboard');
await page.getByTestId('dashboard-ready').waitFor();
await page.evaluate(() => document.fonts.ready);
await page.getByTestId('report-chart').waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot('dashboard.png', {
  fullPage: true,
  animations: 'disabled',
  caret: 'hide',
});

For repeatable regression checks, control or mask content that changes for reasons unrelated to the UI change: current timestamps, rotating promotions, randomized IDs, animated cursors, live counters, and personalized data. Playwright screenshot assertions can apply a stylesheet, and screenshot options can disable animations or hide the caret. Its visual comparison guidance also describes taking captures until two consecutive screenshots match before recording an initial reference. Visual comparisons.

Only hide or mask content when it is outside the purpose of the test. Hiding a changing region makes the test less sensitive to defects in that region.

6. Set diff tolerances with care

Playwright supports a perceived-color threshold and limits such as maxDiffPixels and maxDiffPixelRatio for screenshot assertions. The documented default color threshold is 0.2; it is a versioned API default, not a universal recommendation. Check the documentation for the Playwright version in your project. Screenshot assertion options.

await expect(page).toHaveScreenshot('homepage.png', {
  threshold: 0.2,
  maxDiffPixelRatio: 0.001,
});

Start with a strict comparison, inspect the actual diff, and introduce a tolerance only for understood, irrelevant noise. Raising the threshold can suppress minor antialiasing changes, but it can also conceal real visual defects. Review the diff artifact and the changed area rather than treating a passing threshold as proof that the page is correct.

7. Run and maintain browser-specific baselines

  1. Choose one reproducible host environment for baseline generation and comparison.
  2. Configure explicit browser projects, viewport, device scale factor, and capture settings.
  3. Make the application state deterministic and wait for content to settle.
  4. Generate initial references and inspect each browser’s images.
  5. On each change, compare Chromium to its Chromium reference and Firefox to its Firefox reference.
  6. Inspect failures and image diffs. Update a baseline only after deciding the change is intentional.
  7. When upgrading Playwright, browser builds, the OS image, or fonts, expect possible reference changes; review and regenerate references deliberately.

If the question is specifically whether the browsers differ, use matched viewport, scale, target, content, and state, then compare the paired captures. Treat the difference as a finding to investigate, not automatically as a failure.

8. Capture directly with browser automation

If you do not need visual assertions, Playwright can capture an image directly. This runnable Node.js example writes a full-page screenshot in both browser engines. Install the packages and browser binaries with the commands in section 2, then save this as capture.mjs and run node capture.mjs. Ensure the target site allows access from the machine running the script.

import { chromium, firefox } from 'playwright';

const url = 'https://example.com';

for (const [name, engine] of [['chrome', chromium], ['firefox', firefox]]) {
  const browser = await engine.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 1,
    colorScheme: 'light',
    locale: 'en-US',
    timezoneId: 'UTC',
  });
  const page = await context.newPage();
  await page.goto(url, { waitUntil: 'load' });
  await page.evaluate(() => document.fonts.ready);
  await page.screenshot({
    path: `${name}.png`,
    fullPage: true,
    animations: 'disabled',
    caret: 'hide',
  });
  await browser.close();
}

This uses Playwright’s Chromium build, which is Chromium-based; it is not necessarily the installed Google Chrome application. If the test specifically needs branded Chrome, configure Playwright to use the Chrome channel and keep that version stable as part of the environment. The Firefox capture uses Playwright’s managed Firefox build. See Playwright browser management.

cURL, Python, and Node.js for ScreenshotNeo

These examples call the ScreenshotNeo API to capture a page. They are useful when the goal is a straightforward service-generated screenshot rather than running and maintaining local browser binaries. ScreenshotNeo’s parameter names used by other screenshot APIs also work, which can make an existing integration easier to switch. Review the ScreenshotNeo API documentation for supported request options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.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);

The Node.js example uses Bun’s file writer to save the response. With Node.js, use writeFile from node:fs/promises and convert the response body to a buffer:

import { writeFile } from 'node:fs/promises';

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request returns a PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives Claude, Cursor, and other MCP clients the tools take_screenshot, get_page_info, and capture_pdf.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

There are 1,000 screenshots a month free with no card. Paid plans start at $5 for 3,000 screenshots. Read the API documentation, then sign up for 1,000 free screenshots a month, no card required.

Performance, reliability, and cost

  • Performance: Browser startup and page loading are part of a local automation capture. Reuse a browser process across captures when building a larger capture job, and close contexts and browsers reliably. Full-page shots take and store more pixels than a viewport or component capture; use the smallest region that answers the question.
  • Reliability: Keep the browser, OS image, fonts, viewport, app data, and page state stable. A timeout or intermittent network dependency can produce incomplete content; wait on the app’s actual ready signal and make external data deterministic where possible. Recheck failures before updating references.
  • Cost: Local Playwright has no per-screenshot API charge, but uses CI or developer machine time and storage for browser binaries and image artifacts. For ScreenshotNeo, the free plan is 1,000 shots/month with no card; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Use the usage API to monitor usage.

Troubleshooting

Symptom Likely cause Fix
Every run produces visual diffs The host, browser version, fonts, device scale, viewport, or headless environment differs from the reference run. Restore the same environment and explicit capture settings, or regenerate browser-specific references after reviewing the environment change.
Chrome passes while Firefox fails, or vice versa The engines render differently, or the page has a browser-specific issue. Compare each browser with its own baseline first. Inspect a paired capture to determine whether the difference is an intended engine variation or a defect.
Text wraps differently Different fonts loaded, font loading is incomplete, viewport differs, or a browser font fallback is being used. Use the same available fonts and viewport; await document.fonts.ready; verify the intended font actually loaded in both runs.
Images or charts are missing Capture happened before loading, lazy content was never triggered, or an external request failed. Wait for the relevant image or chart to become ready. Scroll lazy content into view if needed and use stable test data.
Only timestamps, ads, or animated areas differ Dynamic content is outside the intended comparison but remains visible during capture. Freeze its input, disable animation, or hide/mask the specific region with a screenshot stylesheet. Do not mask content that the test is meant to validate.
Screenshot dimensions do not match Viewport, full-page mode, element bounds, or screenshot scale differs. Set the same viewport and scale, and capture the same target type. Confirm both tests select the same element.
Diff passes despite a visible defect Tolerance or maximum difference is too permissive. Lower the threshold or pixel allowance, inspect the diff image, and use a strict check for important components.
Playwright cannot launch a browser The browser binary is missing or the host lacks required dependencies. Install the required browser builds with npx playwright install chromium firefox and use a supported environment. See browser installation guidance.
ScreenshotNeo response is not an image The page may have returned a bot check, blank page, timeout, or failed load; the response includes page-verdict and billing headers. Inspect X-Page-Verdict and X-Billed, then address the page condition or request configuration. See the API docs.

FAQ

Should I compare Chrome screenshots directly with Firefox screenshots?

Only when the test is specifically about cross-browser differences. For regression detection, keep one baseline per browser.

Can one tolerance value work for every page?

There is no universally correct tolerance. Pick it based on the visual importance of the page, inspect image diffs, and revisit it when the rendering environment changes.

Should I use full-page screenshots for every test?

No. Use full-page capture when lower content is part of the question. A viewport or element capture is more focused for above-the-fold or component checks.

Do matching screenshots prove the browsers behave identically?

No. They show that the selected pixels matched under the tested conditions. Functional behavior and untested viewport sizes still need their own checks.