How to Fix an AI Agent Website Screenshot with Fonts That Have Not Loaded
Wait for the target content and used web fonts before capturing with Playwright. Learn how to verify the intended font and diagnose hangs safely.
If an AI agent’s website screenshot shows fallback typography, wait for the page content you need and then wait for the browser’s used fonts to finish loading and layout before capturing. In Playwright, the key step is await page.evaluate(() => document.fonts.ready). If the screenshot call itself hangs while waiting for fonts, diagnose that separately: bypassing the wait can isolate a stalled readiness check, but it does not make the intended font load.
Wait for content, then wait for fonts
Navigation completion and font readiness are separate conditions. First wait for the relevant content or capture target; next wait for the document’s used-font set; then take the screenshot. Replace #capture-target with a selector that exists on the page you are capturing.
import { chromium } from 'playwright';
const url = 'https://example.com';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
try {
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.locator('#capture-target').waitFor({ state: 'visible' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
await browser.close();
}
document.fonts is the page’s FontFaceSet. Its ready promise fulfills after loading and layout operations for the fonts the browser uses are done. It is a useful readiness signal, but it does not prove that every declared font loaded or that the preferred CSS family won the cascade. The used-font set can differ from all declared fonts, including when font-display: optional is involved. If exact typography matters, inspect the affected element and the relevant face status. See [MDN’s Document.fonts reference](https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts).
Verify the intended family and weight
When the screenshot still looks wrong after readiness, check what the element is actually using and whether the expected face loaded. Use the site’s real selector and expected family; the example values below are placeholders.
const fontCheck = await page.evaluate(() => {
const element = document.querySelector('#capture-target');
if (!element) return { error: 'Capture target not found' };
const style = getComputedStyle(element);
const faces = [...document.fonts].map(face => ({
family: face.family,
weight: face.weight,
status: face.status
}));
return {
computedFamily: style.fontFamily,
computedWeight: style.fontWeight,
fontSetStatus: document.fonts.status,
faces
};
});
console.log(fontCheck);
Computed font-family reports the CSS family list, not necessarily a definitive identification of the rendered glyph face. Compare the expected family and weight with the page’s font declarations and the listed face statuses. A face in an error state or a family/weight mismatch points to a font delivery, declaration, or cascade problem that readiness alone cannot correct.
Use targeted waits instead of network quiet
Prefer a wait tied to the content your capture needs. Playwright documents navigation lifecycle states and discourages using networkidle as a general readiness condition for tests; a page can keep network activity open, and network quiet does not establish that the intended font rendered. A content-specific wait followed by document.fonts.ready is more directly tied to this capture’s needs. See the [Playwright Page API](https://playwright.dev/docs/api/class-page).
If a particular face must be checked, you can also ask the page’s font set whether it considers a CSS font shorthand available:
const fontAvailable = await page.evaluate(() =>
document.fonts.check('400 16px "Expected Family"')
);
console.log({ fontAvailable });
Replace the shorthand with the actual weight, size, and family used by the target element. Treat this as one diagnostic signal, not proof that the screenshot’s text used that face: confirm the computed styles, relevant face status, and rendered result for the affected page.
Diagnose a screenshot call that hangs
A font readiness wait can itself stall in a specific browser and automation combination. Playwright issue #42986 is an open report, opened September 29, 2026, describing a Linux WebKit 26.6 run with Playwright 1.63.0 timing out at “waiting for fonts to load” on a public ArcGIS Hub page. The reporter says a Playwright 1.60.0 control completed, but intermediate versions were not bisected and the cause was not established. This bounded report does not show that all Playwright screenshots or WebKit versions have this problem. See the [issue report](https://github.com/microsoft/playwright/issues/42986).
- Keep the normal readiness path first. Reproduce with the target page and record the Playwright version, browser engine and version, operating system, and whether the timeout occurs at navigation, your explicit font wait, or the screenshot call.
- Inspect the font state. If control returns to page evaluation, inspect
document.fonts.status, the relevant face’s status, and the affected element’s computed styles. Check whether the browser or container can retrieve the expected font resource. Do not assume a particular CORS, TLS, cache, or network change is the fix without evidence from the affected setup. - Use the bypass only to isolate the hang. A Playwright-based integration guide documents
PW_TEST_SCREENSHOT_NO_FONTS_READY=1as a way to skip the built-in screenshot font wait when font resources stall. The issue reporter says this let one screenshot complete, but it did not repair the font state; a subsequent normal capture still timed out. A completed bypassed capture may still have fallback or otherwise incorrect typography. See the [integration guide](https://github.com/web-infra-dev/midscene/blob/main/apps/site/docs/en/integrate-with-playwright.mdx). - Restore correctness checks before relying on output. Once the suspected hang is isolated, investigate the actual font face and delivery, then capture through the normal readiness path and verify typography. Do not use bypassed screenshots as proof that the intended font loaded.
The bypass is an environment variable for diagnosis, not a page-level font fix. Follow the integration guide for the runner and shell where it must be set; avoid making it a permanent setting for visual comparisons that depend on correct typography.
Make repeated captures more stable
Font readiness addresses one source of visual variation. Fix the browser, viewport, page state, and test data where practical, and wait for the same meaningful target each time. Playwright screenshot assertions wait for two consecutive screenshots to match; their animation option can disable CSS animations, transitions, and Web Animations. These controls help stabilize captures, but they do not validate the expected font. See the [Playwright PageAssertions API](https://playwright.dev/docs/api/class-pageassertions).
await expect(page).toHaveScreenshot('capture.png', {
animations: 'disabled'
});
Use screenshot assertions when visual comparison is the goal. For a one-off capture, explicitly waiting for the target and fonts before page.screenshot() is usually the simpler path.
Common errors and fixes
| Symptom | Likely explanation | What to do |
|---|---|---|
| Screenshot shows a fallback font | Capture ran before used fonts settled, or the expected face did not load or apply. | Wait for the target and document.fonts.ready; inspect computed family and weight plus face status. |
document.fonts.ready completes, but typography is still wrong |
Readiness covers used-font loading and layout, not whether every declared face loaded or the preferred family won. | Check the intended face’s status, CSS family and weight, and actual font delivery in that browser environment. |
| The locator wait times out | The selector is absent, incorrect, hidden, or the page did not reach the expected content state. | Use a selector from the target page and confirm the page’s content state before waiting for visibility. |
page.screenshot() times out waiting for fonts |
The automation’s built-in font wait may be stalled for this page and browser setup. | Record the environment and inspect font state. Use the documented bypass only to isolate the wait, then restore and verify normal capture behavior. |
document.fonts.check() is true, but the image still looks wrong |
A font-set check does not by itself prove which face rendered every glyph or that the inspected shorthand matches the target style. | Check the affected element’s actual computed styles, expected weight, face status, and rendered screenshot. |
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. A single request can return an image or PDF; its capture options include waiting for a selector, delay, or network idle. The API also accepts parameter names used by other screenshot APIs to make switching easier. See the [ScreenshotNeo documentation](https://screenshotneo.com/docs/).
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie or consent banners as a visitor 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 are not billed, and response headers identify the page verdict and billing status. Its MCP server gives AI agents the take_screenshot, get_page_info, and capture_pdf tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. [Try ScreenshotNeo free](https://screenshotneo.com/account/sign-up/).
Performance, reliability, and cost
Waiting for fonts adds a dependency on the page’s font-loading and layout work, so keep the rest of the readiness checks specific: wait for the content you need instead of waiting for general network quiet without a reason. Set an appropriate timeout for your workflow and log which stage exceeded it so font stalls are distinguishable from navigation or selector timeouts. Do not claim a font is correct merely because a capture returned.
For self-hosted Playwright, the cost and reliability tradeoff is operational: you manage the browser runtime and diagnose the target page’s readiness. ScreenshotNeo is a hosted API alternative with usage-based plan limits: Free is 1,000 shots per month, Starter is $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. Its billing behavior excludes the failure and cache cases described above. These product details are listed at [ScreenshotNeo](https://screenshotneo.com).
FAQ
Does document.fonts.ready wait for every font declared in CSS?
No. It resolves after loading and layout for the used-font set. A declared but unused face may not be part of that set, and readiness does not establish that the preferred family rendered.
Should I use networkidle instead?
Not as a universal font fix. Network quiet and used-font readiness are different signals. Wait for the relevant content and the font set directly.
Does skipping Playwright’s font wait fix missing fonts?
No. It only skips a built-in wait that may be stalled. The resulting screenshot can still show incorrect typography.
Can an AI agent use ScreenshotNeo without automating a browser itself?
Yes. It can call the screenshot API, or use ScreenshotNeo’s MCP server from an MCP-compatible agent client.


