ScreenshotNeo

BlogHow-to

How to Take Consistent Screenshots of a Page With Web Fonts Loaded

Wait for the page state and its required web fonts before capturing. This guide shows reliable Playwright code, font checks, and ways to reduce visual differences.

By the ScreenshotNeo team4 October 20268 min read

To take a consistent screenshot with web fonts loaded, first bring the page into the content state you intend to capture, then wait for the relevant fonts to load and for font-related layout work to settle. In Playwright, the usual document-wide check is await page.evaluate(() => document.fonts.ready). If a particular font face, weight, style, or character subset is essential, request it with document.fonts.load() using a matching CSS font shorthand and representative text, then await document.fonts.ready.

Font readiness does not prove that the intended custom font succeeded: a failed face can leave fallback text on screen. When the intended typeface matters, inspect font-face status or explicitly check the targeted load. The browser’s CSS Font Loading API exposes these states and readiness behavior in MDN’s CSS Font Loading API guide.

1. Capture after the page state and fonts are ready

Here is a runnable Playwright example for Node.js. Install Playwright and its browser with npm install playwright and npx playwright install chromium, save this as screenshot.js, then run node screenshot.js https://example.com. Replace the example URL, main-content selector, font family, and sample text with values from the page you are capturing.

const { chromium } = require('playwright');

async function main() {
  const url = process.argv[2];
  if (!url) throw new Error('Usage: node screenshot.js <url>');

  const browser = await chromium.launch({ headless: true });
  try {
    const page = await browser.newPage({
      viewport: { width: 1440, height: 1000 },
      deviceScaleFactor: 1,
    });

    await page.goto(url, { waitUntil: 'load' });
    await page.getByRole('main').waitFor({ state: 'visible' });

    // If the screenshot includes lazy or conditional content, trigger it here
    // and wait for that content to appear before checking fonts.
    await page.evaluate(async () => {
      await document.fonts.load('400 16px "Example Sans"', 'Representative text 0123');
      await document.fonts.ready;
    });

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

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

The targeted load is optional. Use it when the screenshot has a specific font requirement; substitute the actual family, weight, style, size, and representative characters. If you only need all fonts currently required by the rendered document to settle, await document.fonts.ready without a targeted call.

Playwright’s load navigation condition waits for the page load event; it is not a font-specific assertion. Likewise, networkidle is not a guarantee that the intended font face loaded, and Playwright discourages using it as a test readiness strategy. Prefer a check for meaningful page content and an explicit font readiness check. See the Playwright Page API.

2. Choose the right font readiness check

Approach Use it when What it establishes
document.fonts.ready The screenshot should include all fonts currently needed by the rendered page. The document’s current required font loads and related layout work have settled. It does not establish that a preferred face succeeded.
document.fonts.load(font, text), then document.fonts.ready A named face, weight, style, or character sample is required. Requests matching faces for the supplied font shorthand and text, then waits for the document’s current required font work to settle. Check the result and face status if success is mandatory.

Use CSS font shorthand that matches the page’s actual selection. For example, if the visible heading uses a bold italic face, requesting a regular face does not verify that heading’s face. Include representative characters: pages can use different font subsets for different scripts or glyph ranges.

To examine font-face states in the page, evaluate the document’s font set after the readiness wait:

const fontFaces = await page.evaluate(() => {
  return Array.from(document.fonts, (face) => ({
    family: face.family,
    style: face.style,
    weight: face.weight,
    status: face.status,
  }));
});
console.table(fontFaces);

The CSS Font Loading API defines the states unloaded, loading, loaded, and failed. Treat a failed required face as a capture failure if visual correctness depends on it. If fallback is acceptable for your use case, make that decision explicit instead of silently treating readiness as proof of the preferred font.

3. Stabilize content before checking fonts

Font readiness is scoped to what the document currently needs. A component revealed later can introduce new text and cause another font request. The safe order is:

  1. Navigate to the page.
  2. Trigger the relevant UI state, such as opening a menu, switching a tab, or scrolling to a lazy-loaded section.
  3. Wait for the target content to be visible and rendered.
  4. Request any critical face and sample text, then await document.fonts.ready.
  5. Capture.

For a full-page shot that includes lazy regions, scroll or otherwise trigger those regions and wait for their content before the font check. A screenshot taken before the target text exists cannot ensure fonts needed by that future content have loaded.

A fixed sleep is a poor substitute: it may be too short on a slow run and wastes time on a fast one. A font readiness promise and an assertion for the content you need tie the capture to observable conditions.

4. Make screenshots repeatable beyond fonts

Fonts can change line breaks and page height, but other environment and page differences also affect pixels. For repeatable captures:

  • Keep the browser engine and version, operating-system image, viewport, and device scale factor aligned with the baseline where practical.
  • Choose screenshot output scale deliberately. Playwright’s css scale gives one output pixel per CSS pixel; device uses device pixels.
  • Wait for the meaningful content state instead of relying on DOM creation or a delay.
  • Move the pointer away from hover-sensitive content when hover changes the image.
  • Disable or account for animations and caret blinking; hide or mask volatile areas such as timestamps or rotating content when appropriate.
  • Keep browser settings and capture environment consistent. Operating system, browser/platform, hardware, power source, and headless mode can affect visual output.

Playwright documents screenshot controls and visual comparison considerations in its visual comparisons guide. For baseline checks, Playwright Test’s expect(page).toHaveScreenshot() waits for two consecutive screenshots to match before comparison. That assertion belongs to Playwright Test; it is not a method on every standalone Playwright script. See PageAssertions.

5. Troubleshooting font screenshots

Symptom Likely cause Fix
Screenshot shows fallback typography. The intended font request failed, or the selected face did not match the rendered weight/style. Inspect matching FontFace.status values and browser resource errors. Request the exact family/style/weight with document.fonts.load(); fail the capture if that face is required.
Some text is correct but other characters differ. The page uses another face or subset for those characters. Include representative characters from the affected text in the targeted load and verify the relevant face and resources.
First capture is wrong; a later capture is correct. The capture happened before fonts or content-dependent font requests settled. Wait for the actual content state, trigger lazy content first, then run the targeted load and readiness wait immediately before capture.
networkidle is reached but typography still changes. Network idleness is a network heuristic, not a check that the desired face succeeded or that later UI will not request fonts. Assert the content state and use the Font Loading API. Avoid treating network idle as the font check.
Images differ on another machine although fonts are loaded. Browser, operating system, rasterization, device scale, headless mode, or other environment details differ. Align the capture environment and viewport with the baseline; account for animations, hover, caret, and volatile elements.
Waiting for fonts never completes as expected. A resource may be delayed, blocked, or still requested by changing content. Check browser console and network failures, verify the content is stable, and inspect face statuses. Add a bounded overall timeout to your automation so a genuine failure produces a diagnostic instead of a hung job.

6. Capture versus visual assertion

Use page.screenshot() for one-off captures, custom automation, or when you want to decide yourself how to compare output. Use Playwright Test’s toHaveScreenshot() when maintaining visual baselines and wanting the test assertion to retry until consecutive screenshots stabilize. Both still need the page state and font handling described above. A stable pair of images does not by itself mean the intended font loaded; verify that separately when it is a requirement.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. A single GET request can return a PNG, JPEG, WebP, or PDF. Its screenshot request accepts a URL, so you can capture without maintaining browser automation for the request itself. See the ScreenshotNeo documentation for API options.

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,
)
r.raise_for_status()
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);

These examples use the required API call pattern. Change the target URL and output handling to suit your application. ScreenshotNeo accepts cookie and consent banners like 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, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Sign up for 1,000 free screenshots a month with no card.

8. Performance, reliability, and cost

For browser automation, waiting on the relevant content and fonts avoids arbitrary delays: fast runs can proceed as soon as conditions are met, while slow runs wait for the actual dependency. A targeted font request is useful when one face is critical; document-wide readiness is simpler when all current page fonts matter. Keep a bounded timeout around navigation and capture so blocked resources produce actionable errors.

For visual regression, pinning the browser and operating-system environment reduces avoidable variation, and masking known volatile regions keeps comparisons focused. Font readiness cannot normalize platform rasterization or guarantee identical pixels across different machines.

With ScreenshotNeo, the stated plan counts and prices are available on the product’s plans: Free, 1,000 monthly; Starter, $5 for 3,000; Growth, $15 for 15,000; Pro, $39 for 60,000; Scale, $99 for 250,000; Business, $249 for 1,000,000. Yearly billing gives two months free. Clean shots alone are billed, according to the product facts above. The API reports verdict and billing status in response headers, which helps distinguish a successful billed capture from conditions that cost nothing.

9. FAQ

Does document.fonts.ready guarantee the custom font is being used?

No. It resolves after required font loading and related layout work settles, but a face may fail and text may render with fallback. Check the relevant face status when the chosen typeface is mandatory.

Should I wait for load or networkidle?

Use a content assertion plus an explicit font readiness check. Neither navigation load nor network idleness is a font-specific success condition.

Can I use this method for visual regression tests?

Yes. Perform the same content and font waits before the assertion. In Playwright Test, toHaveScreenshot() can retry until consecutive captures match, but it does not replace checking a required font face.

Why can screenshots still differ after the fonts load?

Browser and operating-system rendering, scale, animations, hover state, dynamic content, and other environmental factors can change pixels independently of font loading.