ScreenshotNeo

BlogHow-to

Why Do Chromium Screenshots Have Missing Fonts? Fix Font Loading

Chromium screenshots can use fallback fonts when a font is missing, lacks the needed glyphs, or has not loaded before capture. Diagnose the cause and fix it in Puppeteer, containers, and web pages.

By the ScreenshotNeo team4 October 202610 min read

Chromium screenshots show missing or unexpected fonts for three main reasons: the browser cannot access the requested font, the font does not include the characters being rendered, or capture happens before the font is ready. A CSS font-family declaration only sets a preferred font stack; it does not confirm that Chromium rendered the preferred face.

First wait for the page content you intend to capture, then wait for document.fonts.ready. If the screenshot still uses the wrong face, investigate the font request, selected family/weight/style, glyph coverage, and fonts installed in the exact runtime environment. The readiness promise is a loading barrier, not proof that the preferred font loaded successfully. MDN’s CSS Font Loading API reference documents font face states and the FontFaceSet.

1. Identify which font problem you have

Check four separate things. Treating them separately avoids repeatedly adding waits when the actual problem is a missing file or glyph.

Check Question Typical symptom
Availability Can the browser fetch the web font, or find the local font in its runtime? Fallback face, failed font request, or missing local typeface.
Coverage Does the selected face contain every character in the captured text? Only certain scripts, symbols, or emoji appear different or missing.
Readiness Had the relevant font finished loading before capture? Capture sometimes shows fallback or invisible text, especially on a slow run.
Application Does CSS select the expected family, weight, style, and subset? A font loads, but bold, italic, or some elements still use another face.

Record the Chromium version, automation library and version, operating system or container image, URL, screenshot options, and the exact text or script that renders incorrectly. Compare local and container runs using the same inputs where possible.

2. Wait for content, then wait for fonts

Wait for an application-specific signal that the content exists before waiting for fonts. A navigation event alone may occur before a client-rendered page has inserted the content you need. Once the target content is present, wait for document.fonts.ready.

Runnable Puppeteer example

This Node.js script opens a page, waits for a target element, waits for font loading to settle, prints font face states, and saves a PNG. Install Puppeteer with npm install puppeteer, then run it with node screenshot.js https://example.com. Replace main with a selector that exists on the page you capture.

const puppeteer = require('puppeteer');

async function main() {
  const url = process.argv[2] || 'https://example.com';
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
    await page.waitForSelector('main', { timeout: 30000 });

    const fontReport = await page.evaluate(async () => {
      await document.fonts.ready;
      return {
        status: document.fonts.status,
        faces: Array.from(document.fonts, face => ({
          family: face.family,
          weight: face.weight,
          style: face.style,
          status: face.status,
        })),
      };
    });
    console.log(JSON.stringify(fontReport, null, 2));

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

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

document.fonts.ready resolves when the document’s font loading has settled and layout is complete. It does not install an absent font and does not establish that every preferred face loaded: inspect face states and browser network activity as well. MDN explains the Font Loading API and face statuses.

Inspect the intended face explicitly

When a particular family, weight, and style matters, ask the browser whether it considers that face available for representative text. This is useful evidence, but combine it with the face report and network diagnostics: a successful check for one string does not establish coverage for every character on the page.

const checks = await page.evaluate(() => ({
  regular: document.fonts.check('16px "Site Sans"', 'Example text'),
  bold: document.fonts.check('700 16px "Site Sans"', 'Example text'),
  arabic: document.fonts.check('16px "Site Sans"', 'مرحبا'),
  faces: Array.from(document.fonts, face => ({
    family: face.family,
    weight: face.weight,
    style: face.style,
    status: face.status,
  })),
}));
console.log(checks);

A true result is not a visual pixel-level assertion that the intended face rendered for all content. Verify the actual font resource and the relevant characters when exact typography is important.

3. Check font requests and CSS selection

  1. In Chromium DevTools or your automation’s request logging, inspect the stylesheet and font file requests. Look for failed, blocked, unauthorized, or unexpectedly redirected requests.
  2. Check the font URL and base path in the deployed page. A relative URL that works during local development may resolve differently in production.
  3. Confirm that the font response is a usable font resource and that the response is accessible to the page. Check any access restrictions, content delivery rules, and browser console errors.
  4. Inspect the applied CSS on the exact text element. Confirm the computed family, weight, style, and any variable-font settings match the faces declared by @font-face.
  5. Check that the stylesheet declares the correct unicode-range, if used. A face can load for one subset while another subset or script falls back.
  6. Use the same text, page state, browser version, and environment when comparing the screenshot with a normal browser rendering.

A stylesheet can declare a family even when its file request fails. A family can also load successfully but lack Arabic, CJK, or other required glyphs. In either case, a readiness wait alone will not produce the intended face.

4. Fix missing fonts in Linux and Docker

Linux containers often differ from developer machines in installed fonts and system dependencies. Check font availability in the same image, as the same user, and in the same runtime that launches Chromium. Puppeteer’s troubleshooting guide lists Linux dependencies and notes that additional font files may be needed for Chinese, Japanese, or Korean characters. Package names and installation commands depend on the distribution and image.

  1. Identify the font family and scripts the page needs. Include the intended font file if the page expects a local system font, and verify that its license allows the deployment use.
  2. Inspect the font files available in the exact runtime image. Do not assume a font installed on the host is visible inside a container.
  3. Install only the system dependencies and script-specific fonts needed for that image. Follow the current instructions for the chosen distribution and Puppeteer setup rather than copying an unrelated package list.
  4. Rebuild the image and reproduce the capture under the same runtime user and launch configuration.
  5. Check whether the font is web-hosted or local. Installing a system font will not repair a failed web-font request; fixing a URL will not add missing glyphs to an installed face.

For a reproducible deployment, make font files and required dependencies part of the image build rather than relying on manual changes to a running container. Keep the set limited to the scripts and typefaces the captured pages require.

5. Configure font loading on the page

Use font-display for visible fallback text

font-display controls how text is displayed while a web font loads. It does not install a font or add missing glyphs. Values such as swap, fallback, and optional allow a system font to be used when the custom face is not ready; that fallback can still look different. Chrome’s font-display guidance describes the tradeoffs, including the potential for layout shifts when the custom font replaces fallback text.

@font-face {
  font-family: "Site Sans";
  src: url("/assets/site-sans.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

Choose a fallback stack that is acceptable for the languages and layout being captured. If exact typography is required, wait for and verify the custom face rather than treating fallback visibility as a fix.

Preload only a critical font when useful

A preload can start fetching a critical font earlier, but it does not guarantee the capture waits for it, and overusing preloads can hurt loading performance. Make sure the preload URL, format, and cross-origin mode match the font request. Evaluate the result in the actual page and capture environment.

<link
  rel="preload"
  href="/assets/site-sans.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

6. Handle dynamic pages and slow font loads

Use an application-specific selector or condition first, then the font barrier. If the page inserts or changes text after the first font wait, wait for that state and wait again before capture.

await page.waitForSelector('[data-report-ready="true"]', { timeout: 30000 });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'report.png' });

A generic fixed delay can hide a race on one machine and still fail on a slower one. Prefer a real page condition and the font loading API. If a font never settles, do not wait forever: set a bounded timeout at the automation layer and inspect failed requests and face states. A timeout gives you a failure to diagnose; it does not make an unavailable font load.

For Puppeteer PDF generation specifically, the PDF option waitForFonts waits for document.fonts.ready and defaults to true. Puppeteer notes that a background page may need to be brought to the foreground if that wait stalls. This is a PDF API note; do not assume it configures every screenshot library. See Puppeteer’s PDF options.

7. Troubleshooting common symptoms

Symptom Likely cause Next check or fix
Every element uses a generic face Font request failed, local face is absent, or CSS does not select the expected family. Inspect font requests and computed styles; fix the URL, access issue, runtime font installation, or CSS declaration.
Only some characters or scripts look wrong The selected font lacks those glyphs, or the required subset is not loaded. Check script coverage and unicode-range; provide an appropriate face for the missing characters.
Text is occasionally invisible or captured in fallback Capture races font loading, or the page uses a font-display behavior that temporarily hides text. Wait for the target content and then document.fonts.ready; inspect @font-face and request timing.
Regular text looks right but bold or italic does not The requested weight or style is undeclared, unavailable, or mapped to a different face. Inspect computed styles and declared face ranges; load or declare the required weight and style.
It works locally but not in Docker The container has different installed fonts, dependencies, permissions, or network access. Inspect the exact image and runtime user; compare requests, font files, and browser versions.
document.fonts.ready resolves but the wrong face remains The intended face failed, is not selected, or lacks the needed glyphs; readiness is not a success assertion. Check face statuses, actual font requests, CSS selection, and coverage.
The wait hangs or reaches a timeout A font or other page work has not settled; a request may be slow or stuck. Use a bounded wait; inspect network activity and page state. For Puppeteer PDF rendering, consult its foreground-page note.
A change to the installed font has no effect The browser is fetching a web font, the wrong image was rebuilt, or the capture runs in another environment. Verify the actual font source and image/runtime used by the capture.

Individual issue reports can help identify a reproducible version-specific behavior, but they are not evidence of a universal Chromium defect. When isolating a report, include browser and automation versions, environment, font state, and request diagnostics.

8. Performance, reliability, and cost

  • Performance: waiting for fonts adds the time needed for outstanding font work to settle. A focused content condition and critical-font strategy are more predictable than a large fixed sleep. Preload only fonts that are actually critical.
  • Reliability: keep the browser version and container image consistent across capture runs. Include needed fonts in the image, and log font face states and failed font requests when a screenshot is wrong.
  • Fallback behavior: font-display can keep text visible while the preferred font loads, but fallback metrics may alter wrapping and layout. Check the resulting screenshot if layout fidelity matters.
  • Cost: font loading itself has no special Puppeteer fee, but longer capture time can consume more of your own compute or job capacity. Diagnose repeated waits and failed requests instead of increasing timeouts without evidence.

9. Or skip the browser setup

For a screenshot without managing a Chromium runtime, call ScreenshotNeo, a website screenshot API and MCP server from Yorker Media. Check the API documentation for request 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,
)
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 and 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 report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

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

FAQ

Does document.fonts.ready prove my custom font loaded?

No. It tells you font loading and layout have settled. Inspect font face status and the actual request, and check that the face covers the text.

Should I use a longer timeout?

Only if diagnostics show that a legitimate request needs more time. A longer timeout cannot supply a missing font or glyph.

Can a system fallback make the screenshot look correct?

Sometimes, if the fallback has suitable glyphs and similar metrics. If the intended typeface is a requirement, verify that face directly.

Is every wrong-font screenshot a Chromium bug?

No. First compare font availability, glyph coverage, readiness, and CSS application in the exact browser and runtime. An issue report alone does not establish a general browser defect.