How to Capture a Web Page Screenshot with Web Fonts Loaded
Wait for the page content, then await `document.fonts.ready` before capturing. Learn how to verify critical fonts, handle failures, and produce repeatable screenshots.
To capture a web page after its fonts have settled, first wait for the content you want to capture, then await document.fonts.ready in the page context immediately before the screenshot. If a particular font face is essential, request it with document.fonts.load() and handle failure. A resolved readiness promise means font loading and related layout have settled; it does not prove that the page rendered a preferred font instead of a fallback.
This guide uses Playwright with JavaScript. It also includes equivalent capture examples for Puppeteer, cURL, Python, and Node.js. The browser automation examples can wait on the page’s font state. A plain HTTP screenshot request does not expose a page context where you can await that browser promise.
1. Wait for page content, then wait for fonts
Navigation milestones such as load and DOMContentLoaded describe document navigation, not whether late content has appeared or the fonts used by that content have settled. Make the content readiness check specific to the page, then await the font set.
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: 'load', timeout: 30_000 });
await page.locator('#capture-area').waitFor({ state: 'visible', timeout: 15_000 });
// Run after the final content is present; it may cause the page to use new fonts.
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'capture.png', fullPage: true, timeout: 30_000 });
} finally {
await browser.close();
}
Install Playwright with npm install playwright. Replace #capture-area with a selector that represents the content in your target page. If your application loads content after navigation, wait for that final state first. A font readiness check performed before late content appears may not cover fonts first used by that content.
The browser’s FontFaceSet.ready promise resolves after document font loading and related layout operations complete, when no further font loads are needed. MDN’s FontFaceSet.ready reference describes this behavior. It is a settling signal, not a guarantee that every declared font loaded or that a particular family won over a fallback.
2. Request a specific critical font
When a screenshot depends on a known face, explicitly request it before the general readiness wait. Use the CSS font shorthand that matches the page’s family, weight, and size. Catch a rejection so the capture job can report or fail on a missing required face.
const fontResult = await page.evaluate(async () => {
try {
const faces = await document.fonts.load('400 16px "Inter"');
await document.fonts.ready;
return { ok: true, matchedFaces: faces.length };
} catch (error) {
return { ok: false, error: String(error) };
}
});
if (!fontResult.ok) {
throw new Error(`Required font could not be loaded: ${fontResult.error}`);
}
await page.screenshot({ path: 'capture.png', fullPage: true });
MDN’s FontFaceSet.load() reference documents that this method forces matching fonts to load and can reject when a requested font fails. A successful call is evidence about that loading operation; it is not proof that every glyph on the page used the intended face. The requested shorthand should match the target’s CSS. A mismatch in family, weight, style, or other font descriptors can mean that the request does not test the face you meant to check.
3. Choose the screenshot scope and output settings
After the page is ready, choose the capture scope that fits the task. Playwright’s Page API supports viewport screenshots, full-page screenshots, and clipped regions, along with output controls such as image type, scale, animation handling, and a screenshot timeout. Fix the viewport and content state when you need repeatable captures.
Full page, viewport, and clip
// Current viewport
await page.screenshot({ path: 'viewport.png' });
// Entire scrollable page
await page.screenshot({ path: 'full-page.png', fullPage: true });
// A CSS-pixel rectangle relative to the page viewport
await page.screenshot({
path: 'header.png',
clip: { x: 0, y: 0, width: 1440, height: 240 }
});
Use fullPage for a page record and a viewport or clip for a fixed visual comparison. A full-page capture can reveal content below the fold that was not visible during the initial readiness check, including lazy-loaded images or sections that load on scroll. If those matter, trigger the page’s relevant loading behavior and wait for the resulting content and fonts before capturing.
Image type, pixel scale, animation, and timeout
await page.screenshot({
path: 'comparison.webp',
type: 'webp',
fullPage: true,
scale: 'css',
animations: 'disabled',
timeout: 30_000
});
Set type to a supported screenshot format such as png, jpeg, or webp when the API and output path support it. Choose the scale according to whether you want CSS-pixel dimensions or device-pixel output. Animation handling can reduce moving or transitional content during a capture; it does not load fonts or establish that the right font face rendered. Set a finite timeout so a stuck screenshot does not hold a worker indefinitely. Refer to the live Playwright Page API for the current option types and defaults.
Visual assertions in Playwright Test
For a Playwright Test visual assertion, toHaveScreenshot() waits until consecutive screenshots are stable before comparing with the expected image. It is a test-runner assertion, not a general-purpose replacement for page.screenshot() in a standalone script. Stabilizing pixels does not by itself establish that a specific named font rendered. Keep the content and font checks in the test as well. See the PageAssertions API.
4. Puppeteer: wait for fonts before capture
The same browser-level approach works with Puppeteer: wait for the target content, await document.fonts.ready in the page, then call Page.screenshot(). Puppeteer also documents element screenshots through ElementHandle.screenshot().
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000 });
try {
await page.goto('https://example.com', { waitUntil: 'load', timeout: 30_000 });
await page.waitForSelector('#capture-area', { visible: true, timeout: 15_000 });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'capture.png', fullPage: true, timeout: 30_000 });
} finally {
await browser.close();
}
Puppeteer’s screenshots guide shows navigation with networkidle2. That is a navigation workflow example; network idleness alone does not validate that the final target content is present or that a preferred font rendered. Use page-specific readiness and the font-settling promise for those checks.
5. cURL, Python, and Node.js with a screenshot API
With browser automation, your code can evaluate document.fonts.ready before capture. With a screenshot API, the capture request runs remotely, so the client cannot directly await that page’s FontFaceSet unless the service exposes an equivalent option. Check the service’s documented readiness controls if you must guarantee custom-font behavior.
ScreenshotNeo offers a one-request screenshot endpoint, with options for waits, custom JavaScript, headers, cookies, viewport, and output format. These client examples show the basic request; they do not claim to add a custom-font readiness wait. See the ScreenshotNeo API documentation for its request options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as output:
output.write(r.content)
Node.js
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 import('node:fs/promises').then(({ writeFile }) =>
writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);
In Node.js versions with top-level await, save the example as an ES module. Keep API keys on the server; do not put a private key in public browser code. For exact web-font readiness in a browser context, use the Playwright or Puppeteer sequence above, or confirm that the remote API has a documented equivalent before relying on it.
6. Or skip the browser setup
If your goal is a clean screenshot without managing a browser process, try ScreenshotNeo. A single GET request returns an image or PDF. Its capture flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each of those steps 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. An MCP server lets Claude, Cursor, and other MCP clients call screenshot, page-info, and PDF tools.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots. See the API docs for options, including waits and custom JavaScript. This one-call example does not demonstrate an explicit document.fonts.ready wait; use browser automation when you need to control that exact page-level step.
Sign up free for 1,000 screenshots a month, with no card required.
7. Troubleshooting font and capture problems
| Symptom | Likely cause | What to do |
|---|---|---|
| Screenshot shows a fallback font | The intended face failed to load, the page used a different face or weight, or the readiness check ran before the final content appeared. | Wait for final content, then await document.fonts.ready. Check failed font requests and request the known face with the exact CSS shorthand using document.fonts.load(). |
document.fonts.ready resolves, but the typeface is still wrong |
Readiness means font loading and related layout have settled; it does not promise that every declared font loaded or that the desired family rendered. | Use document.fonts.load() for a critical face, handle rejection, and inspect the page’s font requests and computed styles. Confirm the target text actually uses that family and weight. |
| The requested font load rejects | The font resource may be unavailable, blocked, misconfigured, or not matched by the supplied shorthand. | Check the browser’s failed font requests, server response, cross-origin configuration, and the CSS family, style, weight, and size. Fail the capture when the face is required. |
| The screenshot captures stale or incomplete content | The capture starts at a navigation milestone before client-rendered or delayed content is ready. | Wait for an application-specific selector or assertion first. Then wait for fonts, since the final content can introduce new font usage. |
| Full-page screenshot misses below-fold content | Lazy-loaded sections or images may not have been activated by the initial viewport. | Scroll or otherwise trigger the page’s loading behavior, wait for the content to appear, then wait for fonts and capture. Use a fixed scope if the task is a viewport comparison. |
| Screenshot operation hangs while waiting for fonts | A browser or version-specific issue, delayed font response, or page behavior may stall the operation. | Record the browser engine and automation version, inspect document.fonts.status and individual face states where possible, check font requests, and reproduce with a minimal page. Keep a finite screenshot timeout. Skipping the wait may emit an image without fixing font state. |
| Network idle never occurs | Analytics, polling, streaming, or other ongoing requests can keep a page active. | Prefer a target-content condition followed by the font readiness promise over using network idleness as the sole readiness check. |
A report filed on September 29, 2026 describes a screenshot timeout while waiting for fonts in Linux WebKit with Playwright 1.63.0, with a passing control run on 1.60.0 under that report’s reproduction conditions. This is a version- and setup-specific issue report, not evidence that WebKit screenshots generally fail. See Playwright issue #42986 and diagnose your own browser and font state before adopting a workaround.
8. Performance, reliability, and cost
- Keep readiness checks narrow. Wait for the content that matters, then wait for font settling. A fixed delay can waste time on fast pages and still be too short on slow ones.
- Set finite timeouts. Bound navigation, selector waits, and screenshot operations so a stalled page or font request does not occupy a worker forever. Choose limits for your environment and record which stage timed out.
- Make captures repeatable. Fix the viewport, device scale, page state, target content, and screenshot scope. Disable or account for animation for visual comparisons. Fonts are only one source of image variation.
- Do not treat network idle as proof. It can be delayed by background traffic and does not establish that the right font rendered. Use application-specific conditions and browser font state.
- Choose the execution model deliberately. Browser automation gives direct control of page readiness and browser options, with the overhead of running a browser. A screenshot API avoids local browser setup but requires the provider to expose the readiness behavior your use case needs.
- Account for API billing rules. ScreenshotNeo bills only clean shots; bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Its free tier includes 1,000 shots monthly without a card; paid plans start at $5 for 3,000. Yearly billing gives two months free, and every feature is on every plan.
9. Frequently asked questions
Does document.fonts.ready wait for every font declared in CSS?
It resolves when document font loading and associated layout are complete and no further font loads are needed. It does not mean every declared face was fetched or used; request a critical face explicitly if that distinction matters.
Can I use a fixed sleep instead?
A sleep only waits for elapsed time. It cannot tell whether the page content or font loading finished. Use a content condition and the font readiness promise for a state-based check.
Should I wait for networkidle?
It may be useful in a workflow, but it is not a font guarantee. Playwright labels networkidle discouraged for testing; prefer web assertions or page-specific readiness conditions, followed by the font check.
Does toHaveScreenshot() guarantee the intended typeface?
No. It waits for screenshot stability in Playwright Test. Check the required font separately when the family itself is part of the assertion.
Can a remote screenshot API guarantee the same font wait?
Only if its documented capture controls provide that behavior. A client making an HTTP request cannot itself evaluate document.fonts.ready inside the remote page.


