ScreenshotNeo

BlogHow-to

How to Fix Playwright Screenshot Differences Caused by Fonts

Wait for web fonts before capturing Playwright screenshots, then isolate browser, font, viewport, and operating system differences that remain.

By the ScreenshotNeo team4 October 20267 min read

To fix Playwright screenshots that differ because fonts load late, wait for document.fonts.ready after navigation and after any interaction that reveals text using additional fonts. Then capture the screenshot. If differences remain, compare the font files and rendering environment: browser version, operating system, viewport, device scale factor, and available fonts.

Wait for fonts before capturing

The browser exposes its font loading state through document.fonts. Its ready promise resolves when fonts used by the document have loaded, related layout work is complete, and no further font loads are needed. It does not promise that every declared font face was loaded; unused faces may remain unloaded.

With Playwright Test, add the wait before the visual assertion:

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

test('page screenshot uses loaded fonts', async ({ page }) => {
  await page.goto('https://example.com');

  // Wait for used fonts and the layout work associated with them.
  await page.evaluate(() => document.fonts.ready);

  await expect(page).toHaveScreenshot();
});

For a direct screenshot, use the same readiness wait before page.screenshot():

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });

try {
  await page.goto('https://example.com');
  await page.evaluate(() => document.fonts.ready);
  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await browser.close();
}

If a click, navigation, or other state change reveals content that uses another font face, wait again after that change and before capturing the updated state:

await page.getByRole('button', { name: 'Open details' }).click();
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'details.png' });

Use screenshot assertions and their options carefully

toHaveScreenshot() retries until two consecutive screenshots match, then compares the final capture with the expected baseline. That retry can help with unstable rendering, but it does not prove that the intended web font loaded. Keep the explicit font wait when font loading is the suspected cause.

Common assertion options affect comparison and capture behavior. Set them only to match the visual contract you intend to enforce:

await expect(page).toHaveScreenshot('page.png', {
  animations: 'disabled',
  scale: 'css',
  maxDiffPixelRatio: 0.01,
});
  • animations controls animation handling during screenshot capture. Disabling animations can reduce unrelated movement in a visual test.
  • scale controls whether the image uses CSS pixels or device pixels. Keep it consistent between baseline and comparison.
  • maxDiffPixelRatio sets an allowed differing-pixel ratio. Choose a tolerance only if that visual variance is acceptable; it cannot fix a missing or incorrect font.

Playwright’s screenshot assertion uses a default perceived-color threshold of 0.2 unless configured otherwise. That threshold is for comparison; raising tolerance can hide small differences, but it cannot make a fallback font become the intended font.

Diagnose differences that remain

First determine whether the page is using the intended font. Inspect computed styles for the affected element and check browser developer tools or network records for the font resource request and its response. Confirm that the file is available in both environments and that the page reaches the same UI state before capture.

document.fonts.check() can be useful as one diagnostic signal, but it does not prove that a particular named font exists or can render the requested glyphs. It answers whether rendering the supplied text would require an unloaded face in the document’s font set; even a missing or nonexistent requested face can still produce true. Use the computed font style and actual font resource loading evidence alongside document.fonts.ready.

Symptom Likely cause First check
Text first appears in a fallback face, then shifts A web font loaded after the initial render Await document.fonts.ready after navigation and after UI changes that reveal text
Font wait finishes, but many glyph shapes still differ Different font file or version, fallback availability, or browser/OS rasterization Compare loaded font resources and pin the browser and CI environment
Text wrapping changes and moves nearby components Different glyph metrics or viewport/scale configuration Hold viewport, device scale factor, browser, and font files constant
Only small edge-level antialiasing differences remain Rendering stack or hardware variation Use the baseline’s environment; consider a comparison threshold only if the residual difference is acceptable

The symptom-to-cause mappings are diagnostic starting points: check the underlying font and environment rather than assuming one cause from the image alone.

Make visual baselines reproducible

A font readiness wait addresses timing inside the page. It cannot make different operating systems or browser builds rasterize text identically. Playwright notes that screenshots may vary with the host operating system, browser version, settings, hardware, power source, and headless mode, and recommends running comparisons in the same environment used to create the baseline.

  1. Generate and compare baselines with the same Playwright browser build and CI image.
  2. Keep browser launch settings and headless mode consistent.
  3. Use the same viewport and device scale factor, and use the same screenshot scale.
  4. Make sure the same font files and fallback fonts are available in both environments.
  5. Wait for fonts after navigation and after UI changes that can reveal newly used fonts.
  6. Investigate font loading and layout before changing visual comparison tolerances.

This combination separates a font-loading race from a stable but different rendering environment. A remaining pixel difference after these controls may be an environment-specific rendering difference rather than a page defect.

Or skip the browser setup

If you need a rendered page image without maintaining a Playwright capture script, ScreenshotNeo provides a website screenshot API and MCP server. Its API captures a URL as PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for parameters and formats.

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);

ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a 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.

Troubleshooting

The screenshot still catches fallback text

Make sure the readiness wait runs after the relevant navigation and after any interaction that reveals the text. If the font resource fails or the page does not request that face for the current content, the wait cannot make it available. Inspect the computed style and font request result.

document.fonts.ready completes, but the expected font is absent

The promise covers fonts used by the document, not every declared face. Check that the affected element’s computed font family is correct, the font file is served successfully, and the requested glyphs are supported. Do not treat document.fonts.check() as proof that the named font exists.

Local screenshots pass, but CI screenshots differ

Compare the browser build, operating system image, installed fonts, viewport, device scale factor, browser settings, and headless mode. Create and compare baselines in the same controlled rendering environment.

Text wraps differently even though the font looks similar

Glyph metrics can change line breaks and the position of downstream elements. Verify the exact font file and version, then make viewport and scale settings identical. Similar-looking fallback fonts can still have different widths.

Only antialiased edges fail the comparison

Confirm first that the intended font and rendering environment match. If the residual perceptual difference is acceptable for the test, adjust the assertion’s comparison threshold deliberately. Avoid broad tolerance changes that could conceal meaningful layout or font regressions.

Performance, reliability, and cost

Waiting on document.fonts.ready adds a readiness dependency to each capture; its completion depends on the page’s used-font loading and associated layout work. This is more targeted to font readiness than relying on an arbitrary fixed delay, though it does not guarantee that every optional or unused face has loaded. A timeout around the overall navigation or test can still be useful to prevent a hung page from blocking a suite indefinitely.

For reliable visual comparisons, control the environment and font files as well as page timing. Keep baselines tied to the browser and CI image that generated them. Comparison thresholds are a policy choice for acceptable visual variance, not a font-loading fix.

The Playwright approach uses the browser font-loading API and Playwright Test; no separate screenshot service is required. If you choose ScreenshotNeo for URL-based captures, its free tier is 1,000 shots per month without a card, and paid plans range from $5 for 3,000 to $249 for 1,000,000 shots. Yearly billing gives two months free, and every feature is on every plan.

FAQ

Does networkidle replace document.fonts.ready?

No. The direct readiness primitive for fonts used by the document is document.fonts.ready. Use it when font timing is the suspected cause.

Should I wait for every declared font face?

Usually the capture needs fonts used by the current page state. The readiness promise resolves for used-font loading and related layout work; unused declared faces may remain unloaded.

Will the font wait make screenshots identical on macOS and Linux?

No. It addresses font loading timing, while operating system, browser build, installed fonts, and rendering stack can still change screenshot output. Generate and compare baselines in a consistent environment.

Does a passing document.fonts.check() prove the font is installed?

No. It is not designed to verify that a specific font exists or supports particular glyphs. Check computed styles and font resource loading as well.