ScreenshotNeo

BlogHow-to

How to Fix Missing Web Fonts in a Playwright Screenshot Used in Documentation

Wait for used fonts with document.fonts.ready before capture. If the screenshot still shows a fallback, trace the font request and CSS actually applied.

By the ScreenshotNeo team4 October 20267 min read

If a Playwright screenshot shows a fallback font, wait for the page’s used fonts and resulting layout to finish before capturing. For a direct screenshot, the essential fix is:

await page.goto(url);
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'documentation.png' });

document.fonts.ready is a readiness signal, not a repair mechanism: it cannot make a failed font request succeed or fix a CSS rule that selects the wrong family, weight, style, or character coverage. If the fallback remains, inspect the font request and the typography actually applied to the text.

1. Wait for fonts before taking a direct screenshot

Navigation reaching its load state and the document’s fonts being ready are separate conditions. Put the font wait immediately before the screenshot so the capture follows the page state you intend to document.

Complete runnable example with Playwright and TypeScript

Install Playwright and its Chromium browser if they are not already installed:

npm install -D playwright
npx playwright install chromium

Save this as screenshot.ts. Replace the example URL with the documentation page you need to capture:

import { chromium } from 'playwright';

async function main() {
  const url = 'https://playwright.dev/';
  const browser = await chromium.launch();

  try {
    const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
    await page.goto(url);

    // Wait for fonts used by this document and the resulting layout.
    await page.evaluate(() => document.fonts.ready);

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

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run it with a TypeScript runner such as npx tsx screenshot.ts after installing tsx with npm install -D tsx. The core fix is the page-side wait; the browser launch and cleanup make the example runnable as a script.

The promise concerns fonts used by the document and completed layout operations. It does not require every declared @font-face to load: an unused face may never be needed by the current content. See MDN’s documentation for FontFaceSet.ready and Document.fonts.

2. Diagnose why the screenshot still uses a fallback

If the screenshot remains wrong after the readiness wait, check the page and font itself. The specific cause depends on the site’s CSS, network responses, and browser; no single wait can diagnose those for you.

  1. Capture the exact page state. Navigate to the documentation route and state you intend to show. If the relevant text appears only after interaction or conditional rendering, make it appear before checking the font.
  2. Confirm the text is rendered. A font face can be declared but unused until matching content appears. Lazy or conditional content may not have triggered its font request yet.
  3. Wait for readiness just before capture. Use await page.evaluate(() => document.fonts.ready) after the relevant content is present.
  4. Inspect font requests and browser errors. In browser developer tools or the page’s network and console diagnostics, check whether the expected font file was requested and whether it failed. A failed request needs a page or delivery fix; waiting cannot recover it.
  5. Check the matching CSS rule. Verify the target text’s computed font-family, weight, and style match an available @font-face declaration. Also check that the font includes the characters shown. A declared face with a different weight, style, or character coverage may leave the browser using a fallback.
  6. Check font loading status and the rendered result. The Font Loading API exposes faces, statuses, and loading errors. Use those along with the target element’s computed typography and what the browser actually renders; do not rely on a single boolean check.

document.fonts.check() has limited semantics: it indicates whether rendering specified text would require an unloaded face that could cause a swap. It is not proof that a particular named face exists or is the face actually rendered. Consult MDN’s CSS Font Loading API and FontFaceSet references when diagnosing a page-specific issue.

3. Use the right readiness signal

Signal What it tells you Use it for this issue?
document.fonts.ready Used fonts have loaded and layout operations have completed. Yes. Await it before a direct screenshot.
Navigation load The navigation’s document load event has occurred. It does not replace the font readiness check.
networkidle A network activity state, not proof the intended font rendered. Do not use it as a substitute for the specific readiness condition. Playwright discourages it for testing.
waitForTimeout() A fixed delay elapsed. Not a reliable production fix. Playwright describes timer-based waits as inherently flaky and says not to wait for a timeout in production.

Use the condition tied to the work under test: here, the document’s fonts being ready. See the Playwright Page API guidance on screenshots and waiting.

4. If you use Playwright Test screenshot assertions

For a visual assertion, wait for font readiness and then use toHaveScreenshot() to compare the result with a stored baseline:

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

test('documentation page renders with its intended fonts', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await page.evaluate(() => document.fonts.ready);
  await expect(page).toHaveScreenshot('documentation.png');
});

The assertion repeatedly captures until two consecutive screenshots match, then compares the last capture with the baseline. That stabilizes the screenshot comparison; it does not prove the intended web font loaded. Keep baseline generation and comparison in a consistent environment: operating system, browser version, rendering settings, hardware, power source, and headless mode can affect the result. Review the Playwright guides to visual comparisons and PageAssertions.

5. Performance, reliability, and cost

The readiness wait is tied to font loading and layout, rather than an arbitrary sleep duration. Its completion time depends on what the page needs to load and whether requests succeed; a slow or broken font still needs diagnosis. Keep the screenshot flow focused on the actual page state and font dependency.

For reliable visual baselines, use a consistent browser and host environment, and investigate whether a difference is a real page change or a rendering environment change. The research sources provide no benchmark or fixed timing guarantee for font readiness, so do not assume a universal capture duration.

For direct Playwright capture, the operational cost is your browser execution and maintenance; no third-party screenshot API is required for this fix. If you instead use a hosted screenshot API, compare its behavior, billing rules, and options against your needs. ScreenshotNeo documents its API and its plan prices on its own site.

6. Troubleshooting checklist

Symptom Likely cause What to do
Screenshot uses a fallback even after navigation completes Document load completed before the used font and layout were ready. Await document.fonts.ready in the page before capture.
Fallback remains after the readiness promise resolves The font request failed, the CSS selection does not match, or the face lacks needed characters. Inspect font requests/errors, the target’s computed family/weight/style, and character coverage.
A declared font never appears in diagnostics The face may not have been used by the rendered content. Ensure the relevant text is present and rendered before waiting; the readiness promise does not require every declared face to load.
document.fonts.check() returns true but the face looks wrong The check does not prove a named face exists or is rendered. Inspect face status, CSS selection, and the browser’s rendered result.
Screenshot assertions differ across machines Rendering environments differ, or the page state is unstable. Keep browser, OS, settings, and capture environment consistent; verify font readiness and review baseline changes.
A longer fixed sleep appears to help intermittently The timing race remains and the delay is not tied to font completion. Replace the sleep with the font readiness condition and diagnose failed requests if needed.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its API returns an image or PDF from one GET request, and its docs cover the request options. For a screenshot request:

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://playwright.dev/"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://playwright.dev/' });
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 and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before 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 AI agents tools to take screenshots, get page information, and capture PDFs. 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 for 1,000 screenshots a month, with no card required.

8. FAQ

Why does my Playwright screenshot use a fallback font?

The font may not have loaded when capture occurred, or the page may be selecting a different face than expected. Await document.fonts.ready, then inspect the font request and the target text’s CSS selection if the fallback remains.

How do I wait for fonts to load before taking a Playwright screenshot?

Run await page.evaluate(() => document.fonts.ready) after navigating and rendering the relevant content, then call page.screenshot().

Does document.fonts.ready guarantee that every font in my CSS loaded?

No. It resolves when fonts used by the document and the resulting layout are ready; unused declared faces need not load.

Does toHaveScreenshot() fix missing fonts?

No. It waits for consecutive captures to stabilize before comparing to a baseline, but you still need to confirm the intended face loaded and keep the rendering environment consistent.