Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If html2canvas uses a fallback font, shifts text metrics, or drops text in Chrome, first wait for the browser’s font set and layout to settle. Await document.fonts.ready; explicitly request the required face with document.fonts.load() when needed; verify the real font request in DevTools; then compare captures with foreignObjectRendering disabled. If fonts are loaded and the result still differs, you may be seeing an html2canvas CSS or SVG rendering limitation rather than a Chrome font-loading failure.

What html2canvas is—and why a correct page can produce a wrong font

html2canvas does not copy the browser’s pixels. It reconstructs a canvas from the target DOM and the CSS properties it implements. Chrome is supported, but support does not mean that every CSS feature, font configuration, or combination of options will match the onscreen page. The project FAQ puts the limitation plainly: “Every CSS property must be manually implemented to render correctly, so html2canvas will never have full CSS support.”

That leaves three broad causes for an @font-face mismatch:

  • The webfont was still loading when capture began, so fallback metrics were captured.
  • The requested family, weight, style, or document font set is not the one you inspected.
  • The font is ready, but html2canvas cannot reproduce the relevant CSS, SVG text, or renderer mode.

Diagnose them in that order. Do not assume that a browser that displays the live page has supplied html2canvas with the same resources at the same time.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

1. Wait for used fonts before calling html2canvas

document.fonts.ready resolves after used fonts finish loading and related layout work completes. Put the await immediately before capture, not only during initial page setup.

async function captureElement(element) {
  await document.fonts.ready;
  return html2canvas(element);
}

const canvas = await captureElement(document.querySelector('#invoice'));
document.body.appendChild(canvas);

The promise concerns fonts the document actually uses. Unused or optional faces can remain unloaded, so this is necessary but not always sufficient when your element introduces a particular weight or text string only at capture time.

2. Explicitly load the exact family, weight and style

Use FontFaceSet.load() with a CSS font shorthand that matches the rule applied to the element. The second argument is sample text; include representative characters, especially if a subsetted font is involved.

async function captureWithBrandFont(element) {
  try {
    await document.fonts.load('600 16px "Brand Sans"', 'Invoice total €0123456789');
    await document.fonts.ready;
  } catch (error) {
    console.error('Brand Sans failed to load', error);
    throw error;
  }

  return html2canvas(element, {
    foreignObjectRendering: false
  });
}

A rejected load() promise is a real failure signal: inspect the request, response and console error rather than silently capturing with a fallback. If your CSS uses italic, variable-font axes, or another weight, request that exact face instead of assuming a neighboring face will be substituted correctly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why document.fonts.check() is not proof

document.fonts.check() answers whether rendering can proceed without an unloaded face causing a swap. It can return true when a nonexistent family would be rendered with a usable fallback. Treat it as a pending-load diagnostic, not as evidence that the intended file, family or weight exists.

3. Verify the browser request, not just the CSS declaration

  1. Open Chrome DevTools and select the Network panel.
  2. Reload with the panel open, filter by Font, and locate the request for the family and weight used by the target element.
  3. Confirm the response succeeds, the URL is the expected file, and the request is not blocked by a policy, redirect, authentication failure or incorrect MIME configuration.
  4. Check the Console for decode, CORS, stylesheet or certificate errors.
  5. In the Elements panel, verify the captured node’s computed font-family, font-weight and font-style.
  6. Ensure the node belongs to the same document whose document.fonts set you awaited. An iframe has its own document and its own font set.

Historical html2canvas reports describe fallback metrics when capture started before webfonts finished loading. They are useful clues for a timing investigation, not proof that every current release mishandles cached fonts.

4. Isolate foreignObjectRendering

foreignObjectRendering is false by default. Compare both modes in a controlled reproduction, but record your html2canvas and Chrome versions because old reports are version-specific.

Test What it tells you Interpretation
Default or false Uses html2canvas’s normal DOM/CSS reconstruction. If the font is correct here, the foreign-object path is implicated.
true Uses an SVG foreign-object route for supported HTML. Differences may reflect browser security, SVG or CSS support, not font timing.
const normal = await html2canvas(element, {
  foreignObjectRendering: false
});

const foreignObject = await html2canvas(element, {
  foreignObjectRendering: true
});

A Chrome 75 report against html2canvas 1.0.0-rc.3 described Google Fonts failing with the option enabled but working when it was removed. Another report, involving Chrome 77 and Firefox 69, described fonts and images being fetched yet absent from the output. Those reports were opened in 2019 and do not establish a current universal Chrome defect. Reproduce with your current versions before choosing a mode.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

5. Keep image CORS settings separate from font diagnosis

The useCORS option tells html2canvas to attempt loading images with CORS. Its documented use and FAQ guidance concern cross-origin images, response headers and proxying. It is not a general @font-face switch. Turning it on will not repair a font request that failed, selected the wrong face, or completed after capture.

Investigate a font through the CSS Font Loading API and the font’s own Network request. Handle image failures independently so you do not mistake a missing background image for a font-rendering problem.

6. Reduce the page to a minimal reproduction

When the font is demonstrably ready but output remains wrong, remove variables until the failing feature is obvious.

  1. Create a small element containing ordinary HTML text, one @font-face family and one weight.
  2. Capture it with foreignObjectRendering: false.
  3. Add the second weight, then italic, variable-font settings and text effects one at a time.
  4. Test SVG text separately from HTML text. Older issue reports specifically involved @font-face and SVG text; separating the element type helps identify an SVG limitation.
  5. Reintroduce external images, filters, transforms and complex layout only after the basic text matches.

This process distinguishes a failed resource from a CSS feature that html2canvas does not implement. Do not promise support for every SVG/font combination without testing the current browser and library versions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reliable capture pattern

async function renderInvoice() {
  const element = document.querySelector('#invoice');
  if (!element) throw new Error('Missing #invoice');

  // Request the face actually used by the element.
  await document.fonts.load('400 16px "Brand Sans"', 'Invoice preview');
  await document.fonts.load('600 16px "Brand Sans"', 'Total €0123456789');
  await document.fonts.ready;

  const canvas = await html2canvas(element, {
    foreignObjectRendering: false,
    backgroundColor: '#ffffff'
  });

  return canvas.toDataURL('image/png');
}

renderInvoice()
  .then(dataUrl => {
    document.querySelector('#preview').src = dataUrl;
  })
  .catch(error => {
    console.error('Capture failed', error);
  });

Capture only after content that changes layout has settled. If your page injects text, toggles a class, or swaps a font after the awaits, wait for that operation and the resulting layout before invoking html2canvas.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting by symptom

Symptom Likely cause Action
Fallback face and shifted line breaks Capture raced font loading. Await document.fonts.load() for the exact face, then document.fonts.ready; inspect the Font request.
Only one weight is wrong The requested weight is missing or being synthesized. Check the CSS rule and Network response for that weight; load it explicitly.
check() says true but the brand face is absent Fallback can render without the named family. Use Network and computed styles; do not treat check() as existence proof.
Normal mode works, foreign-object mode fails Renderer-path or SVG/CSS compatibility issue. Keep foreignObjectRendering: false and test a current library/browser pair.
HTML text works, SVG text fails SVG text or its CSS/font combination is outside the implemented path. Use a minimal SVG reproduction or convert the design to ordinary HTML where possible.
Font request is absent The stylesheet was not applied, the face is unused, or the element is in another document. Check stylesheet loading, iframe context, computed styles and explicit load().
Images are missing while text is correct Cross-origin image policy, not @font-face. Handle image CORS headers or proxying separately; useCORS applies to images.

Performance, consistency and version notes

  • Font waits are asynchronous and normally cheaper than debugging a capture whose dimensions changed after rendering. Avoid arbitrary sleeps when the Font Loading API can express the condition you need.
  • Cache your loaded faces in application state if several captures use the same document; do not repeatedly trigger needless loads.
  • Record the html2canvas version, Chrome version, renderer option and font URLs with bug reports. Historical issue behavior from Chrome 75, Chrome 77 or old release candidates cannot be generalized to current builds.
  • Use a small reproduction before changing unrelated options. A successful font request does not guarantee that html2canvas implements every CSS property used by the page.

Or skip the browser setup

For a server-side screenshot rather than a canvas reconstructed in the browser, ScreenshotNeo accepts one GET request and returns PNG, JPEG, WebP or PDF. It is #1 for screenshot APIs here because it removes consent banners, newsletter popups and chat widgets before capture, bills only clean shots, and has the lowest paid plan.

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)
open("shot.webp", "wb").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}`);

See the ScreenshotNeo documentation for the 63 capture options, including full-page and element captures, device and retina settings, custom CSS and JavaScript, waits, request blocking, cookies and headers, PDFs, caching, signed links, asynchronous jobs, bulk capture and usage reporting. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed; response headers identify the page verdict and billing result. 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 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Does waiting for document.fonts.ready load every font declared in CSS?

No. It waits for fonts used by the document; unused or optional faces may remain unloaded. Request a needed face explicitly with document.fonts.load().

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I enable foreignObjectRendering to improve font fidelity?

Not by default. It is false by default, and historical reports show version-specific failures when enabled. Compare both modes on your current versions and keep the one that reproduces your target reliably.

Can useCORS fix a missing webfont?

No. useCORS is documented for images. Diagnose @font-face through the Font Loading API, Network panel and font response itself.

The Bottom Line

Synchronize the exact font face before capture, verify its request, compare renderer modes, and reduce the page to a minimal case. If the font is loaded yet html2canvas still differs, treat the result as an implementation limitation rather than endlessly changing font timing.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.