ScreenshotNeo

BlogHow-to

How to Fix Text Shadow Rendering Bugs in html2canvas

Diagnose blurry, offset, or missing text shadows in html2canvas by checking the CSS property, font readiness, render scale, and cloned DOM.

By the ScreenshotNeo team30 September 202612 min read

How to Fix Text Shadow Rendering Bugs in html2canvas

If a CSS text-shadow looks blurry, displaced, or missing in an html2canvas output, isolate the property first, wait for web fonts to finish loading, then compare an explicit scale: 1 render with the default scale. html2canvas lists text-shadow as supported, but reconstructs a canvas from DOM and style information instead of asking the browser for a literal screenshot. Support therefore does not guarantee a pixel-identical match for every font, browser, and output scale. The supported CSS feature list and project documentation describe those distinctions.

This guide gives a small reproduction, a diagnostic sequence, complete browser-side code, and targeted fixes. Keep the exact CSS, html2canvas version, browser, and scale in each comparison so a change actually tests one hypothesis.

1. Confirm you are debugging text-shadow

text-shadow paints a shadow around glyphs. box-shadow paints around an element’s box. They are separate CSS properties with different support status: html2canvas lists text shadows as supported and box shadows as unsupported. A dark halo on the card boundary, rounded corners, or the element background is not evidence that the text shadow itself failed. Filters and blend effects are separate again; the feature list marks filter and mix-blend-mode unsupported.

.headline {
  color: #fff;
  font: 700 48px/1.15 Arial, sans-serif;
  text-shadow: 2px 3px 5px rgba(0, 0, 0, 0.45);
}

.card {
  /* This is an element shadow, not a glyph shadow. */
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}

Inspect the computed style on the affected text node in the browser’s developer tools. Confirm that text-shadow is non-none, and record the complete computed value, including each layer if the declaration has multiple shadows. If the visible defect follows the card edge rather than the letters, reduce the reproduction to the text shadow and troubleshoot the box effect separately.

2. Make a minimal, repeatable reproduction

Capture one short text string in a plain element. Keep its font family, font size, weight, color, line height, and shadow declaration fixed. Remove animation, transforms, filters, complex stacking, and unrelated layout while narrowing the cause. Save a browser screenshot or reference image and compare it with the generated canvas at the same viewport and device-pixel ratio.

First establish a baseline with no shadow. Then add the exact shadow declaration. If the text itself already has different width or position without a shadow, investigate font loading or layout before shadow blur. If only the halo changes, scale is a useful next controlled variable.

<div id="capture">
  <p class="headline">Shadow reproduction</p>
</div>

Keep the page’s dimensions stable during the comparison. A changed viewport can alter wrapping, media queries, and the captured element’s size, which makes shadow comparisons ambiguous. Record the html2canvas version, browser and operating-system versions, viewport, device-pixel ratio, and whether the intended font had loaded.

3. Test scale and blur as separate variables

The html2canvas configuration reference documents scale with a default of window.devicePixelRatio. Consequently, two machines or displays can produce canvases with different pixel dimensions from the same CSS layout. A project pull request specifically records a fix for a text-shadow blur-radius mismatch related to scale; that change history makes scale worth testing, but does not establish that every current release has the same defect. See pull request 3083.

Compare the default device-pixel scale with an explicit scale while keeping the CSS fixed.
Compare the default device-pixel scale with an explicit scale while keeping the CSS fixed.

Render at scale: 1, then render again with the default (omit scale). Do not simultaneously change the blur, font, or viewport. Compare the output’s dimensions as well as the perceived shadow. If scale changes the symptom, retain the smallest reproduction and report the exact scale and html2canvas release. Avoid compensating with a random CSS blur adjustment: it can hide a scale-specific discrepancy and then fail at another output size.

const target = document.querySelector('#capture');

const baseline = await html2canvas(target, {
  scale: 1,
  logging: true
});

const defaultScale = await html2canvas(target, {
  logging: true
});

console.log({
  baseline: [baseline.width, baseline.height],
  defaultScale: [defaultScale.width, defaultScale.height],
  devicePixelRatio: window.devicePixelRatio
});

For a production image, choose the scale based on the required pixel dimensions and memory budget, then keep it explicit for repeatable output. A scale above one increases both canvas width and height, so pixel count and memory rise approximately with the square of the scale. Very large canvases can become slow or exceed browser canvas limits; do not raise scale as a generic fix for a shadow mismatch.

4. Wait for fonts before rendering

A fallback font can change glyph width, baseline, and outline. Since a text shadow follows the glyph rendering, the result can look like a shadow offset problem even when the root cause is that the intended web font was not ready. A historical report for html2canvas 1.0.0-rc3 described squished or displaced text while fonts were downloading. This is a diagnostic lead, not proof of a current defect; reproduce it on the version you use. See issue 1940.

A font that is not ready can change both glyph placement and the shadow around it.
A font that is not ready can change both glyph placement and the shadow around it.

In modern browsers, wait for the document’s font set before capturing. If a particular face matters, ask the font set to load that face and verify it is available. Keep this wait before the screenshot call, and handle rejection or timeout according to your application’s fallback policy.

async function waitForCaptureFonts() {
  if (!document.fonts) return;

  await document.fonts.ready;
  await document.fonts.load('700 48px "Brand Sans"');

  if (!document.fonts.check('700 48px "Brand Sans"')) {
    throw new Error('Brand Sans is not available for capture');
  }
}

await waitForCaptureFonts();
const canvas = await html2canvas(document.querySelector('#capture'), {
  scale: 1,
  logging: true
});

Replace Brand Sans and the font descriptor with the face and weight actually used by the target. If the page intentionally uses a system fallback, remove the explicit font load and still wait for document.fonts.ready where supported. Recheck the computed font-family on the clone as well as on the live page.

5. Inspect the cloned document

html2canvas works from a cloned document. Its onclone callback receives that document and may modify the captured copy without changing the original. The documented logging option can expose renderer activity. Use both to check that the clone contains the same text, class names, computed font styles, and shadow declaration you expect. Configuration details are in the official options reference.

const canvas = await html2canvas(document.querySelector('#capture'), {
  scale: 1,
  logging: true,
  onclone: (clonedDocument) => {
    const clonedTarget = clonedDocument.querySelector('#capture');
    const clonedText = clonedDocument.querySelector('.headline');

    console.log('clone target:', clonedTarget);
    console.log('clone text:', clonedText);
    if (clonedText) {
      const style = clonedDocument.defaultView.getComputedStyle(clonedText);
      console.log({
        textShadow: style.textShadow,
        fontFamily: style.fontFamily,
        fontSize: style.fontSize,
        fontWeight: style.fontWeight,
        color: style.color
      });
    }
  }
});

Open the clone in your browser’s debugging workflow when practical, or temporarily add a visible diagnostic outline/background in the clone. Do not leave diagnostic styles in a production capture. If the clone lacks a class or style, find out why it was not copied or whether application code changes the DOM while capture is underway.

6. Complete runnable browser example

This minimal page pins html2canvas to a release, waits for fonts, captures the target, and downloads a PNG. For a production build, manage the dependency through your package manager and use the version already approved by your project. The example uses a system font so it does not rely on an external font request.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>html2canvas text shadow reproduction</title>
<style>
  #capture { padding: 32px; background: #26354a; }
  .headline {
    margin: 0;
    color: white;
    font: 700 48px/1.15 Arial, sans-serif;
    text-shadow: 2px 3px 5px rgba(0, 0, 0, .45);
  }
</style>
<div id="capture"><p class="headline">Shadow reproduction</p></div>
<button id="download">Save PNG</button>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script>
  document.querySelector('#download').addEventListener('click', async () => {
    try {
      if (document.fonts) await document.fonts.ready;
      const target = document.querySelector('#capture');
      const canvas = await html2canvas(target, {
        scale: 1,
        logging: true,
        backgroundColor: null,
        onclone: (doc) => {
          const text = doc.querySelector('.headline');
          if (text) console.log('clone shadow:',
            doc.defaultView.getComputedStyle(text).textShadow);
        }
      });
      const link = document.createElement('a');
      link.download = 'shadow.png';
      link.href = canvas.toDataURL('image/png');
      link.click();
    } catch (error) {
      console.error('Capture failed:', error);
    }
  });
</script>
</html>

The example is for diagnosing text shadow, not a guarantee that every CSS feature on a complex page will be reproduced. It sets a transparent canvas background when no background is supplied; use a color such as #fff if you need an opaque output. For very large images, prefer a Blob workflow over a data URL to avoid holding a large base64 string in memory.

7. Relevant options and common edge cases

Option or condition When to use it What to watch
scale Compare device-pixel ratio with a fixed output density. Changes output pixel dimensions and can affect blur appearance.
width, height Control the canvas dimensions for a known target. Incorrect dimensions can crop content; preserve a stable viewport too.
windowWidth, windowHeight Reproduce media-query layout conditions. Different viewport values can change wrapping and font size.
x, y, scrollX, scrollY Diagnose crop position or fixed-position content. These affect the captured region/position, not CSS shadow support.
backgroundColor Set a predictable backdrop or use null for transparency. Transparency may make a shadow look different against the viewer’s background.
logging, onclone Inspect renderer activity and the cloned styles. Disable noisy logging after diagnosis; do not mutate the live page.
useCORS, proxy, allowTaint Investigate cross-origin images in the same capture. Image loading and canvas readability are separate from text-shadow rendering.

Multi-line text, wrapped lines, letter spacing, transformed ancestors, clipped containers, and several layered shadows add variables. Reproduce the simplest case first, then add each feature back one at a time. Text outside the chosen element or canvas bounds may be clipped even if its shadow rendering is correct. A transparent background can also make a faint shadow hard to see; compare over the same solid color.

8. Troubleshooting: symptom, cause, fix

Symptom Likely cause to check Next action
Shadow blur differs from browser view Scale/device pixel ratio changes; scale-related rendering difference. Compare explicit scale 1 and omitted scale with every other input fixed; report the result.
Text and shadow both look squished or shifted Fallback font or different font metrics. Wait for document.fonts.ready, verify the face with document.fonts.check, then inspect clone styles.
Shadow absent in the output Wrong property, missing cloned style, empty/hidden text, or unsupported surrounding effect. Confirm computed text-shadow; remove filters and isolate a plain text node.
Dark edge around a panel is missing The style is box-shadow, which the feature list marks unsupported. Keep this distinct from text shadow; consider a supported visual treatment or another capture method.
Output has clipped letters or halo Target/canvas bounds or crop dimensions are too tight. Increase target padding or correct width, height, x, and y; compare full target bounds.
Capture rejects or image is blank Another resource or rendering problem, possibly a cross-origin image or timing issue. Read console/logging output, wait for required assets, and investigate CORS separately.
Works on one machine only Different browser, DPR, font availability, or viewport. Record and align those values before calling it a library regression.

Do not infer a current universal browser ranking from one reproduction. The available project materials do not establish that one browser or one scale is always correct. Treat browser, operating system, library version, scale, and font readiness as recorded test inputs.

9. Performance, reliability, and cost

html2canvas runs in the browser and its work grows with the amount of DOM and pixels it must render. Capture the smallest useful element, avoid unnecessary repeated captures, and keep output scale to the actual resolution requirement. Large canvases consume memory; a high scale multiplies both dimensions. When capturing repeatedly in a long-lived single-page application, check the options reference for cache controls such as clearImageCache and maxCacheSize, especially if the page also contains many images.

For reliability, wait for fonts and any relevant page content, avoid changing layout during capture, and add error logging around the promise. If output must be consistent, fix the html2canvas version, browser environment, viewport, and scale. A browser-generated canvas is not a substitute for a native screenshot when exact browser pixels are required; html2canvas reconstructs the page from supported DOM and style information.

There is no per-capture html2canvas service charge in this client-side workflow, but it still costs browser time, memory, and engineering effort to handle rendering differences and asset loading. Select the method based on whether you need an in-page export or a screenshot of a rendered website.

10. Report a reproducible bug

  1. Reduce the page to a single text node and its exact shadow declaration.
  2. Include a browser screenshot and generated output at matching dimensions.
  3. State html2canvas version, browser and operating-system versions, viewport, DPR, and explicit scale (or say the default was used).
  4. Say whether the intended font was loaded before capture and provide the computed font and shadow values.
  5. Include console output and the smallest runnable HTML/CSS/JavaScript reproduction.
  6. Confirm the issue on the latest release you can use and distinguish it from box-shadow, filters, and crop behavior.

That evidence lets maintainers separate a reproducible renderer issue from missing fonts, unsupported effects, or environmental differences. Historical reports can suggest checks, but should not be treated as proof that an old defect persists today.

Or skip the browser setup

For a screenshot of a live website, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request returns PNG, JPEG, WebP, or PDF. It renders the page in a browser, so it can be useful when you need a page screenshot rather than a client-side DOM reconstruction. It does not repair html2canvas output for a locally generated component or guarantee identical rendering for every page.

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/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server exposes 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. See the ScreenshotNeo API documentation.

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); // In Node.js, use fs/promises to write the response bytes.

For Node.js, save the response with the built-in file-system API:

import { writeFile } from 'node:fs/promises';

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 writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Use your API key in a server-side environment; do not embed a secret key in public browser code. Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.

FAQ

Does html2canvas support text-shadow?

Yes. Its feature list includes text-shadow. Support does not promise pixel identity across all fonts, browsers, and scales.

Should I set scale to 1?

Use it as a diagnostic comparison. Choose a production scale based on desired output dimensions and test it in the target environment.

Is text-shadow the same as box-shadow?

No. Text shadow follows glyphs; box shadow follows the element box. The feature list treats them differently.

Can ScreenshotNeo capture an element rendered only in my local app?

The API captures a URL. A local-only element that is not available at a URL is outside that workflow; use html2canvas for client-side element capture.