Wait for the page state that uses the typeface, then await document.fonts.ready inside the browser before capturing. For a font family, weight, style, or character subset that must be present, explicitly call document.fonts.load() and handle a rejected load. The reliable order is application content, font readiness, then screenshot—not navigation followed immediately by an image.
The reliable Playwright sequence
A typical capture looks like this:
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Wait for your route, hydration, API data, or other final content state here.
await page.locator('[data-testid="report"]').waitFor();
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();
document.fonts.ready resolves when the document’s currently used fonts and associated layout operations are ready. It is a browser API exposed through Playwright’s page context, so the callback must run with page.evaluate(). See the Playwright Page API and MDN’s FontFaceSet.ready reference.
Why the order matters
Font usage is determined by the content that is actually rendered. A single-page app may add headings after hydration; a data table may introduce a weight that was not used on the initial route; and a modal may add a different character range. Navigate, wait for that final state, and only then await readiness. Waiting immediately after goto() can settle too early for content that has not yet been inserted.
Load a particular face or text subset explicitly
document.fonts.ready concerns fonts currently used by the document. It does not promise that every @font-face declared in a stylesheet—including unused faces—has downloaded. If your screenshot must contain a specific family, weight, style, or script, force that match with FontFaceSet.load():
#1 Best Overall
await page.evaluate(async () => {
await document.fonts.load('600 16px "ExampleFont"', 'Dashboard Revenue 2026');
});
await page.screenshot({ path: 'dashboard.png' });
The first argument is a CSS font specification. The second is representative text: include characters from the subset that will appear in the screenshot. MDN documents that load() fulfills with the matching loaded FontFace objects and rejects when loading fails. Let that rejection fail the test (or catch it and report a clear diagnostic) rather than silently capturing a fallback font.
Multiple weights and scripts
await page.evaluate(async () => {
await Promise.all([
document.fonts.load('400 16px "Inter"', 'Body copy and labels'),
document.fonts.load('700 16px "Inter"', 'Heading and totals'),
document.fonts.load('400 16px "Noto Sans Arabic"', 'مرحبا بالعالم')
]);
});
Request each face your visual assertion depends on. A bold heading can otherwise use a synthetic or fallback weight while normal text uses the intended family.
A complete reusable helper
Centralize the wait so every screenshot follows the same policy. The helper below waits for a selector, waits for used fonts, optionally requests explicit faces, and captures with animations disabled.
Rank #2
import { chromium } from 'playwright';
async function capture(url, options = {}) {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
if (options.readySelector) {
await page.locator(options.readySelector).waitFor({ state: 'visible' });
}
// The application state is now the one that will appear in the image.
await page.evaluate(() => document.fonts.ready);
if (options.fonts?.length) {
await page.evaluate(async (fonts) => {
await Promise.all(fonts.map(({ spec, text }) =>
document.fonts.load(spec, text)));
}, options.fonts);
}
await page.screenshot({
path: options.path ?? 'page.png',
fullPage: true,
animations: 'disabled'
});
} finally {
await browser.close();
}
}
await capture('https://example.com/report', {
readySelector: '[data-testid="report"]',
fonts: [
{ spec: '700 16px "Inter"', text: 'Quarterly revenue' }
],
path: 'report.png'
});
The animations: 'disabled' option handles CSS animations, transitions, and Web Animations during the screenshot operation. It solves a different problem from font readiness; use both when the page has both moving elements and web fonts. The option is documented in the Page API.
Free tools Windows power users keep installed
One-click scans. No signup required.
Screenshot assertions: useful, but not a font wait
With Playwright Test, expect(page).toHaveScreenshot() waits for two consecutive screenshots to match before comparing the final image. That stability check can reduce noise from changing pixels, but its official description does not make it a replacement for an explicit font wait.
import { test, expect } from '@playwright/test';
test('report uses the loaded typeface', async ({ page }) => {
await page.goto('/report');
await page.getByTestId('report').waitFor();
await page.evaluate(() => document.fonts.ready);
await page.evaluate(() => document.fonts.load('700 16px "Inter"', 'Quarterly revenue'));
await expect(page).toHaveScreenshot('report.png', {
animations: 'disabled',
fullPage: true
});
});
Keep the explicit readiness step before the assertion. Consecutive identical captures can still be identical while both use a fallback face.
Choosing the right waiting strategy
| Approach | Scope | Best use | Limitation |
|---|---|---|---|
await document.fonts.ready |
Fonts currently used by the document and related layout completion | Rendered content determines the required faces | Unused declared faces may remain unloaded |
await document.fonts.load(font, text) |
Faces matching the CSS specification and text | A named family, weight, style, or subset is essential | It can reject; choose an accurate specification and representative text |
In most tests, use both: wait for the final application state, await ready, then explicitly load only the faces that are contractual for the image.
Diagnose font-related screenshot failures
The screenshot contains a fallback font
- Cause: capture occurred before the component rendered or before the font request completed.
- Fix: wait for the component’s visible selector, then run
await page.evaluate(() => document.fonts.ready). Adddocument.fonts.load()for a required face.
Only bold or italic text is wrong
- Cause: the requested weight or style has a separate font file or is being synthesized.
- Fix: load the exact CSS specification, such as
700 16px "Inter"oritalic 400 16px "Inter", with representative text.
load() rejects
- Cause: the family name, weight, URL, CORS policy, or font file is invalid or unavailable.
- Fix: inspect the browser console and network response, verify the CSS name exactly (including spaces and quoting), and correct server CORS and font MIME configuration. Do not turn the rejection into a successful-looking screenshot.
The wait never completes or the test times out
- Cause: a font request is hanging, the page is continually changing, or the application never reaches its ready state.
- Fix: use a bounded Playwright timeout, investigate the stalled font request, and wait on a deterministic application selector rather than an arbitrary long delay. A fixed sleep can hide a slow network and still be too short on another run.
Local and CI images differ
- Cause: different browser versions, viewport, device scale, operating-system fallback fonts, network responses, or animation state.
- Fix: pin the Playwright browser version used by CI, set viewport and scale deliberately, make font assets reachable in the test environment, disable animations, and wait for the same application state in both environments.
Some characters still use another face
- Cause: Unicode-range subsetting or a missing glyph causes per-character fallback.
- Fix: pass representative characters from every script to
document.fonts.load(), verify the subset file contains those glyphs, and check the rendered text rather than loading only the Latin sample.
Performance and reliability considerations
Waiting for fonts adds only the time needed for the required assets and layout; it is more predictable than adding a large fixed delay. Keep the sequence narrow: wait for a deterministic content marker, then readiness, then explicit faces. Avoid forcing every declared font when the screenshot uses only two; unnecessary downloads increase test time and create more failure points.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Font readiness cannot freeze other dynamic inputs. Images, client-side data, ads, timers, animations, and personalized responses can still change pixels. Combine font waits with route or data assertions, stable test data, a controlled viewport, and disabled animations. For a full-page image, remember that lazy content may render while Playwright scrolls; ensure your application has finished loading the content you intend to assert.
Rank #4
Or skip the browser setup
ScreenshotNeo provides a website screenshot API when you do not need to maintain a Playwright browser. Its capture request can return PNG, JPEG, WebP, or PDF, and its cleaning step accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; each response reports the result with X-Page-Verdict and X-Billed headers.
For a direct request, see the ScreenshotNeo documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The same call in 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()
open("shot.webp", "wb").write(r.content)
And 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 failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Plans include 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan. If you want to avoid browser setup, create a free ScreenshotNeo account.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Frequently Asked Questions
Does document.fonts.ready load fonts that are not visible?
Not necessarily. It tracks fonts used by the document and related layout work; unused declared faces can remain unloaded. Request a required face explicitly with document.fonts.load().
Should I use a fixed timeout instead of a font wait?
No. A fixed delay is dependent on network and machine speed. Use an application-state assertion followed by document.fonts.ready, with explicit load() calls for contractual faces.
Can screenshot assertions replace document.fonts.ready?
No. toHaveScreenshot() waits for consecutive captures to stabilize, while document.fonts.ready addresses font loading and layout readiness. Use both when appropriate.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →

