ScreenshotNeo

BlogAI agents

How to Fix an AI Agent Screenshot with a Missing Web Font

Diagnose fallback fonts and font-wait timeouts in AI agent screenshots. Use Playwright to check font loading, wait for the intended face, and capture reliably.

By the ScreenshotNeo team4 October 20267 min read

A screenshot with the wrong typeface usually means the page was captured before its web font loaded, the font request or CSS face failed, or the browser’s screenshot readiness wait did not settle. First render the intended page state, inspect the font request and face state, then wait for the specific font before capturing. If the font is still unavailable, waiting longer alone will not fix delivery or CSS configuration.

1. Check whether the intended font loaded

Web fonts load asynchronously. A page can display fallback text while a custom face is unavailable; the chosen font-display behavior affects what readers see during loading. A screenshot taken during this interval can preserve the fallback appearance. Chrome’s font-display guidance explains how these choices affect text visibility.

After the route and the component that uses the font have rendered, inspect:

  • document.fonts.status: whether the document font set is loading or has settled.
  • document.fonts.check(...) and document.fonts.load(...): whether the expected family and style are available for representative text.
  • The font request in browser network diagnostics: response status, URL, and any network or CORS failure.
  • The computed CSS family, weight, and style on the actual element. A loaded regular face does not prove that the requested bold or italic face loaded.

A failed request or face points toward delivery or font configuration. A set that stays in loading while screenshot preparation waits suggests a readiness or browser-runtime problem. These checks narrow the cause; no single check identifies every possible failure.

2. Wait for fonts after the page reaches its final state

Run the font wait after navigation and after application code has rendered the element that needs the font. This Playwright example checks a specific face, reports failed faces, and gives the wait a finite timeout. Save it as screenshot.mjs; install Playwright with npm install playwright and run node screenshot.mjs https://example.com.

import { chromium } from 'playwright';

const url = process.argv[2] ?? 'https://example.com';
const fontFamily = 'Avenir Next';
const fontSpec = `16px "${fontFamily}"`;
const sampleText = 'Representative text for this page';
const browser = await chromium.launch();

try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });

  // Replace this condition with an application-specific ready marker when needed.
  await page.locator('body').waitFor({ state: 'visible', timeout: 15_000 });

  const fontReport = await page.evaluate(async ({ fontSpec, sampleText }) => {
    await document.fonts.load(fontSpec, sampleText);
    await document.fonts.ready;
    const faces = [...document.fonts].map(face => ({
      family: face.family,
      style: face.style,
      weight: face.weight,
      status: face.status,
    }));
    return {
      status: document.fonts.status,
      requestedFaceAvailable: document.fonts.check(fontSpec, sampleText),
      faces,
    };
  }, { fontSpec, sampleText });

  console.log(JSON.stringify(fontReport, null, 2));
  if (!fontReport.requestedFaceAvailable) {
    throw new Error(`Required font is unavailable: ${fontSpec}`);
  }

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

Use your real family name, weight, style, and representative text. If the application loads a font only after a route transition, user action, or component render, perform that step first. For a font declared with @font-face, the Font Loading API can request the face before capture; the browser still must be able to fetch and use it. Check face states and the request rather than treating a completed promise as proof of the intended visual result.

3. Diagnose Playwright screenshot timeouts

Playwright screenshot capture can wait for fonts as part of its readiness work. A reported issue describes a specific timeout in Linux WebKit with Playwright 1.63.0 and WebKit 26.6; the report’s Playwright 1.60.0 control completed. It is an open, limited reproduction, not evidence that all projects or versions have the same defect. The report describes timing-sensitive face states and does not identify a responsible WebKit change. See Playwright issue #42986 for its setup and caveats.

When the screenshot call stalls, record Playwright and browser versions, operating system or container image, screenshot timeout, document.fonts.status, and each relevant face’s status. Compare a minimal reproduction in the same environment. A reported diagnostic used PW_TEST_SCREENSHOT_NO_FONTS_READY=1 to let one capture proceed, but the next normal capture timed out again. Skipping the wait can help isolate a readiness hang; it does not load the intended font or prove the resulting image is correct.

Playwright’s screenshot assertion disables animations by default, but animation handling and font readiness are separate matters. Review the PageAssertions screenshot options if an assertion behaves differently from a direct screenshot.

4. Fix font delivery when the page is yours

  1. Open the font request and verify the URL resolves successfully in the capture environment.
  2. Check network logs for blocked requests, CORS errors, redirects, or an inaccessible host.
  3. Match the @font-face family, weight, and style declarations to the CSS actually applied to the target element.
  4. Provide a deliberate fallback stack. Choose font-display behavior based on the page’s needs; swap, fallback, or optional can expose fallback text while the custom face is unavailable.
  5. Capture only after the application has rendered and the required font face has loaded, or explicitly report a missing face instead of silently accepting a fallback.

Fallback fonts can have different metrics, which changes line wrapping and layout even when the text remains visible. A fallback screenshot is therefore not necessarily a reliable visual baseline.

5. Keep visual comparisons reproducible

Use the same operating system, browser version, settings, and headless configuration for the reference and current images. Playwright notes that host OS, browser version, settings, hardware, power source, and headless mode can affect rendering; its visual comparisons guide recommends matching the screenshot environment. Repeated matching screenshots can help a page settle, but do not establish that the intended font face loaded.

Observation Likely next check
Font request fails or face status is error Fix URL, access, CORS, or the @font-face declaration.
Font set remains loading Identify which face is still pending and whether the screenshot wait is stuck.
Font set is loaded but text looks wrong Inspect computed family, weight, style, selector specificity, and whether the desired face covers the text.
Capture completes only when font readiness is skipped Treat it as a diagnostic of the wait path; verify font state and image fidelity separately.
Images differ across machines Match browser, OS/container, headless mode, and settings before comparing.

6. Troubleshooting

The screenshot contains fallback typography

Cause: capture happened before the requested face loaded, or that face could not be selected. Fix: wait after final rendering, request the exact family/weight/style, inspect face status and network delivery, then verify computed styles.

document.fonts.ready resolves, but the wrong font appears

Cause: readiness does not guarantee that the particular face and glyphs you expected were successfully used. Fix: check the desired face with document.fonts.check, inspect face errors, and confirm computed CSS and representative text coverage.

page.screenshot() times out waiting for fonts

Cause: a face may remain pending, or the browser/runtime may have a readiness issue. Fix: gather face states and version details, set a finite timeout, and create a minimal reproduction. Compare versions only as a diagnostic; the Linux WebKit report is specific and does not establish a universal version recommendation.

The font works locally but fails in CI

Cause: the CI container may differ in network access, certificates, OS, browser build, or configuration. Fix: inspect the request and face state inside that exact environment, then keep the screenshot baseline and capture environment aligned.

Waiting longer does not help

Cause: a timeout cannot repair a broken URL, blocked request, incorrect face declaration, or a browser wait that never settles. Fix: distinguish delivery failure from a readiness stall using request logs, face states, and a bounded wait.

7. Performance, reliability, and cost

Waiting only for the required face avoids adding an arbitrary long sleep to every capture. Use a finite timeout so broken font delivery fails visibly instead of holding an automation job indefinitely. For stable visual checks, pin the browser/container environment used by the baseline and record changes when upgrading it. The research does not establish a general performance benchmark or a universal cost for these approaches; runtime and infrastructure costs depend on the project’s own browser workload.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. A single request captures a URL as PNG, JPEG, WebP, or PDF. It removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed; responses include page-verdict and billing headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the ScreenshotNeo site and API documentation.

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

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

FAQ

Does a screenshot timeout mean the font is broken?

No. It can indicate a failed font, a face that is still loading, or a browser readiness problem. Check the request and face states to distinguish them.

Does disabling animations fix missing fonts?

No. Animation handling and font readiness are separate capture concerns.

Should I skip the browser’s font wait in production?

Only if accepting an unverified fallback is appropriate for your use case. Skipping a wait may let capture finish, but it does not restore the intended typeface.