ScreenshotNeo

BlogHow-to

How to Fix Playwright Screenshots That Show the Wrong Font

Wait for the page’s final content and used web fonts, then verify the selected face and match the browser environment. Here’s how to diagnose wrong fonts and screenshot hangs.

By the ScreenshotNeo team4 October 20269 min read

Start by waiting for the page’s final content, then wait for the document’s used fonts to finish loading before capturing. In Playwright, run await page.evaluate(() => document.fonts.ready) after the content that uses the custom font appears. If the result still looks wrong, check whether the intended font request succeeded and whether the element uses the expected family, weight, and style. Then compare the local and CI browser and operating system. A fixed sleep or a stable screenshot does not prove that the intended font rendered.

document.fonts.ready resolves when loading and layout operations for the document’s used fonts are complete. It does not guarantee that every declared font loaded or that the browser chose the face you expected; some declared faces may be unused or unavailable. MDN’s Document.fonts reference explains the API and this distinction.

1. Wait for content, then wait for fonts

Navigation and font readiness are separate. A page can reach a navigation milestone before the final content is rendered or before the font used by that content has settled. Wait for a meaningful page condition first, such as a heading or component becoming visible. Then wait for fonts immediately before the screenshot.

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

test('captures the page after its web fonts settle', async ({ page }) => {
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  // Replace this with a selector that signals the content is ready.
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();

  await page.evaluate(() => document.fonts.ready);

  await page.screenshot({ path: 'page.png', fullPage: true });
});

Use your application’s actual readiness signal in place of the example URL and heading. If the relevant text appears only after client-side rendering, waiting for a selector before the font wait ensures the browser has had a chance to use the face. Playwright discourages using networkidle as a general test-readiness condition; prefer an assertion or event tied to the intended UI state. See Playwright’s Page API guidance.

For a visual assertion, keep the same ordering:

await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('welcome.png');

toHaveScreenshot() retries until it gets two consecutive matching screenshots. That helps with repeatability; it does not verify that the preferred font loaded. See Playwright’s visual comparisons documentation.

2. Confirm which font the browser actually used

When waiting does not fix the appearance, inspect the target element’s computed style and the browser’s font faces. Computed font-family shows the CSS family list, not always the physical font file used for every glyph. Combine it with request and response details from Playwright’s network events and the face status exposed by the CSS Font Loading API.

const fontReport = await page.evaluate((selector) => {
  const element = document.querySelector(selector);
  if (!element) return { error: `No element matches ${selector}` };

  const style = getComputedStyle(element);
  return {
    text: element.textContent,
    fontFamily: style.fontFamily,
    fontSize: style.fontSize,
    fontWeight: style.fontWeight,
    fontStyle: style.fontStyle,
    fontFaces: [...document.fonts].map((face) => ({
      family: face.family,
      weight: face.weight,
      style: face.style,
      status: face.status,
    })),
  };
}, '.hero-title');
console.log(fontReport);

Capture font requests and failures while navigating. This example logs matching requests and responses; adapt the predicate if your app serves fonts through a proxy or uses another extension.

page.on('request', (request) => {
  if (/\.(woff2?|ttf|otf)(\?|$)/i.test(request.url())) {
    console.log('FONT REQUEST', request.url());
  }
});
page.on('response', (response) => {
  if (/\.(woff2?|ttf|otf)(\?|$)/i.test(response.url())) {
    console.log('FONT RESPONSE', response.status(), response.url());
  }
});
page.on('requestfailed', (request) => {
  if (/\.(woff2?|ttf|otf)(\?|$)/i.test(request.url())) {
    console.error('FONT FAILED', request.failure()?.errorText, request.url());
  }
});

Check the relevant @font-face declaration and CSS usage together:

@font-face {
  font-family: 'Site Sans';
  src: url('/fonts/site-sans-regular.woff2') format('woff2');
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

.hero-title {
  font-family: 'Site Sans', Arial, sans-serif;
  font-weight: 400;
}

Verify the family spelling, weight and style descriptors, URL, response status, CORS behavior when the font is hosted on another origin, and that the file exists in the test environment. A request that succeeds can still serve the wrong file or a face whose descriptors do not match the requested weight or style. Check that the text is not intentionally using a fallback for glyphs absent from the font.

To ask the browser to load a particular face for a specific text sample, use the font set’s load() method, then inspect the result. The shorthand must match the intended style and weight:

const loadedFaces = await page.evaluate(async () => {
  const faces = await document.fonts.load('400 16px "Site Sans"', 'Sample text 0123');
  await document.fonts.ready;
  return faces.map((face) => ({
    family: face.family,
    weight: face.weight,
    style: face.style,
    status: face.status,
  }));
});
console.log(loadedFaces);

This prompts a load for the specified font request and text, but it cannot make a missing file available or prove that every character uses that face. The browser may use fallback fonts for unsupported glyphs. The CSS Font Loading API documents face states and loading methods at MDN.

3. Make local and CI rendering comparable

If the screenshot looks right locally but wrong in CI, record the browser engine and version, operating system or container image, headless mode, font availability, font request results, and the page state at capture time. Run visual comparisons in the same environment used to generate the baselines, or keep separate baselines for intentionally different browser/platform projects. Playwright notes that rendering can vary with the host OS, browser version, settings, hardware, power source, headless mode, and other factors. Its snapshot names can distinguish browser and platform or use project names when multiple projects are configured. See the Playwright visual comparison guidance.

Pin the Playwright version and use its matching browser installation in CI. Ensure the font files are present in the deployed test environment and that network policy permits their requests. If the site uses system fonts, the operating system’s installed fonts are part of the rendering environment; a CSS family name alone does not install a font.

Snapshot options such as style or stylePath can apply CSS during capture to hide volatile content or standardize page styling. They do not supply a missing font. Likewise, increasing a pixel-difference threshold can mask a difference but does not correct the font. Separate platform baselines when those differences are expected instead of treating distinct renderers as pixel-identical.

4. Diagnose a font wait that hangs

If document.fonts.ready never resolves, find which faces are still loading and whether requests failed. A wait with a timeout helps keep a diagnostic run from stalling indefinitely, but timing out is not evidence that the page has correct fonts.

const fontState = await page.evaluate(async (timeoutMs) => {
  const result = await Promise.race([
    document.fonts.ready.then(() => ({ ready: true })),
    new Promise((resolve) => setTimeout(() => resolve({ ready: false }), timeoutMs)),
  ]);

  return {
    ...result,
    status: document.fonts.status,
    faces: [...document.fonts].map((face) => ({
      family: face.family,
      weight: face.weight,
      style: face.style,
      status: face.status,
    })),
  };
}, 10000);
console.log(fontState);

A timed diagnostic can identify a face left in a loading state, but do not proceed as if the font is verified when ready is false. Inspect failed and pending requests, reproduce in a minimal page if possible, and compare the exact engine and version.

A scoped report opened September 29, 2026 describes a Linux WebKit screenshot wait hanging in Playwright 1.63.0 in a particular environment. It is an issue report, not proof of a general Playwright problem. Check the report’s current status and confirm that your version and engine match before attributing a hang to it: Playwright issue #42986.

The report says PW_TEST_SCREENSHOT_NO_FONTS_READY=1 allowed capture in its diagnostic, but did not repair font state or guarantee settled fonts. Treat such a bypass only as a way to isolate whether the wait is blocking; it is not a font-correctness fix. If you use it for diagnosis, verify font requests and rendered output separately before relying on the screenshot.

5. Troubleshooting common wrong-font symptoms

Symptom Likely cause What to check or change
Screenshot uses a visibly different typeface The intended font request failed, the family name differs, or the requested face descriptor does not match. Inspect computed family, weight and style; check font request failures and response status; compare @font-face descriptors with CSS usage.
Works locally, falls back in CI Different browser/OS or missing local font files, or CI cannot fetch the remote font. Match and pin the browser/container environment; ensure assets exist and are reachable; compare request logs.
Some characters look wrong while others look right The selected font may not contain all glyphs, so fallback renders a subset. Inspect the exact characters and font coverage; include representative text when checking a face with document.fonts.load().
Font is correct sometimes, wrong on first capture The capture ran before the final text or font load settled. Wait for the relevant content selector, then await document.fonts.ready immediately before capture.
Screenshot assertion passes, but font is wrong Repeated output is stable but consistently uses a fallback. Verify face state, computed styles and network requests; screenshot retries only establish repeatability.
Capture times out while waiting for fonts A face may be stuck loading, or an engine/version-specific issue may apply. Log face states and font requests, compare the exact browser environment, and check for a matching known issue. Do not treat bypassing the wait as proof of correctness.
Changing the screenshot tolerance appears to fix it The assertion permits more pixel differences; font loading may still be wrong. Fix the underlying font or environment mismatch. Tune tolerance only for expected, understood rendering variation.

6. Performance and reliability considerations

Waiting for used fonts adds whatever time their outstanding loading and layout work needs. The cost is usually tied to font delivery and page readiness, so avoid adding arbitrary long sleeps to every capture. Use one readiness check at the point it matters and investigate unusually slow requests or a face that never settles.

Remote font delivery adds a network dependency. For repeatable CI captures, make the font asset available consistently and keep browser/container versions stable. A cache can reduce repeated transfers, but cache state should not be the only reason a visual test passes: a clean environment should be able to fetch the required font too.

For diagnosis, preserve the useful evidence: the screenshot, browser/version and platform, computed style, font-face status, and font request outcomes. That makes intermittent failures easier to distinguish from a consistent fallback or a renderer difference.

7. Or skip the browser setup

If you need a screenshot without maintaining a Playwright browser environment, ScreenshotNeo provides a website screenshot API and MCP server. The API captures a URL in one GET request; see the ScreenshotNeo API documentation for options.

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)
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}`);
  • Cookie and consent banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Responses identify the page verdict and billing status in headers.
  • An MCP server gives AI agents, including Claude and Cursor, the tools take_screenshot, get_page_info, and capture_pdf.
  • 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.

Sign up for 1,000 free screenshots a month, with no card required.

8. FAQ

Does document.fonts.ready prove the exact font rendered?

No. It settles loading and layout for used fonts. Check the face state, computed styles, network outcome, and glyph coverage to confirm the intended face.

Should I use waitUntil: 'networkidle' before a screenshot?

Not as a universal readiness signal. Prefer a condition that proves the content you need is present, then wait for fonts.

Can toHaveScreenshot() fix a wrong font?

No. It retries for matching captures. It can make a consistently wrong result repeatable.

Why does only one weight look wrong?

The weight may be undeclared, mapped to another face, or requested with a weight outside the face’s descriptor range. Check the CSS declarations and the computed weight.

Is a font-wait bypass safe for production snapshots?

It can let a diagnostic capture proceed, but it does not establish that fonts settled. Verify the rendered font independently before trusting the image.