ScreenshotNeo

BlogHow-to

Why SVG Elements Render Incorrectly with html2canvas and How to Fix Them

SVGs can look correct in a browser yet render blank, incomplete, or differently in html2canvas. Find the causes, diagnostics, fixes, and reliable alternatives.

By the ScreenshotNeo team1 October 20269 min read

Short answer: html2canvas does not photograph the browser surface. It walks the DOM and recreates what it can on a canvas. SVG features that the browser paints natively—such as unsupported CSS, filters, masks, clip paths, external references, fonts, or embedded foreignObject content—can therefore be missing or incorrect. Cross-origin images and fonts can also be skipped or taint the canvas. Reduce the SVG to a minimal fixture, verify every referenced resource, configure CORS or a same-origin proxy, test foreignObjectRendering deliberately, and validate the result in every browser engine you support.

The html2canvas documentation describes the output as DOM based and says it may not be 100% accurate to the page’s real representation. Its FAQ explains why: every CSS property must be implemented manually, so full CSS support is impossible. See the documentation and FAQ.

What html2canvas is actually doing

When you call html2canvas(element), the library clones the document, reads styles and layout, loads readable resources, and paints an approximation into a canvas. It is not equivalent to a Chromium, Firefox, or Safari screenshot API. A browser can render an SVG through its native graphics stack even when html2canvas has no implementation for one of the SVG’s features.

This distinction explains several common reports:

  • Missing SVG: the element contains a feature html2canvas cannot parse or paint, or a required resource failed.
  • Blank SVG: a resource error, unsupported filter or mask, foreignObject failure, or a canvas size limit prevents useful output.
  • Wrong styles or fonts: styles are external, computed values are unsupported, the font is unavailable in the cloned document, or a known foreignObject browser difference is involved.
  • Security error from toDataURL(): a cross-origin image or SVG resource tainted the canvas.
  • Different browser results: the fallback renderer and SVG/foreignObject support differ between Chromium, Firefox, and Safari.

Diagnostic workflow

  1. Create a minimal fixture. Keep one <svg> with a path, solid fill, stroke, and plain text. Temporarily remove filters, masks, clip paths, CSS variables, pseudo-elements, external <use> references, images, web fonts, and embedded HTML.
  2. Capture the fixture with logging. Confirm basic paths and fills before adding one feature back at a time.
  3. Inspect every resource. Check SVG images, CSS background images, fonts, linked stylesheets, and <use> targets in the Network panel. Record the final URL after redirects and inspect response headers.
  4. Test the two renderer paths. Compare the default renderer with foreignObjectRendering: true on the same fixture and target browser.
  5. Check the cloned document. Use onclone to inline a fallback font or remove animation only in the clone.
  6. Check viewport and canvas dimensions. Set windowWidth and windowHeight to the dimensions needed by the element, then check browser canvas limits if output is blank or truncated.
  7. Keep a cross-engine regression set. Render the same small fixtures in the Chromium, Firefox, and Safari versions that matter to your users.

Minimal reproducible example

<!doctype html>
<html>
<body>
  <div id="target">
    <svg width="320" height="160" viewBox="0 0 320 160">
      <rect width="320" height="160" fill="#f3f4f6" />
      <path d="M30 120 L100 35 L170 120 Z" fill="#2563eb" stroke="#111827" stroke-width="4" />
      <text x="190" y="90" font-family="Arial, sans-serif" font-size="20" fill="#111827">Basic SVG</text>
    </svg>
  </div>
  <button id="save">Capture</button>
  <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
  <script>
    document.querySelector('#save').addEventListener('click', async () => {
      const node = document.querySelector('#target');
      const canvas = await html2canvas(node, {
        logging: true,
        windowWidth: node.scrollWidth,
        windowHeight: node.scrollHeight,
        onError: error => console.warn('html2canvas resource failed', error)
      });
      const link = document.createElement('a');
      link.download = 'svg-fixture.png';
      link.href = canvas.toDataURL('image/png');
      link.click();
    });
  </script>
</body>
</html>

If this fixture works, add your real SVG features back individually. The first feature that changes the output identifies the likely implementation or resource boundary.

Fix unsupported SVG features

Filters, masks, clip paths, and CSS variables

Reduce complex effects to a simpler equivalent for capture. For example, replace a filter shadow with a precomputed shadow shape, inline critical fill and stroke values, and avoid relying on CSS variables until the base rendering is known to work. This is a compatibility workaround, not a guarantee that every SVG feature is supported.

External <use> references

Inline the referenced symbol into the captured SVG when possible. External references add another resource lookup and can fail in the cloned document even though the live page displays them.

Embedded HTML and foreignObject

Try the alternate renderer explicitly:

const canvas = await html2canvas(node, {
  foreignObjectRendering: true,
  backgroundColor: null,
  logging: true,
  onError: error => console.warn(error)
});

This can improve CSS fidelity in browsers that support the capability, but it is not a universal fix. Reports include blank output or errors with this mode and incorrect font colors for foreignObject nested inside SVG. Test it against your minimal fixture in every target engine.

Fix images, fonts, and CORS

When an SVG contains an image, CSS background, font, or linked file, the capture context must be able to read it. html2canvas warns that cross-origin images can taint the canvas and are skipped when allowTaint is false.

Use CORS only when the server permits it

const canvas = await html2canvas(node, {
  useCORS: true,
  logging: true,
  onError: error => console.warn('resource failed', error)
});

useCORS: true requires the asset response to include an appropriate Access-Control-Allow-Origin header. It cannot grant permission that the asset server did not send.

Use a same-origin proxy when you control neither asset headers nor redirects

const canvas = await html2canvas(node, {
  useCORS: true,
  proxy: '/image-proxy',
  logging: true,
  onError: error => console.warn(error)
});

The official examples document both CORS and proxy approaches. A subtle failure occurs when a URL starts same-origin and redirects to a CDN: an issue records that the initial origin check can prevent crossOrigin from being applied, so the final response still taints the canvas. Inspect the final network URL. Prefer a final CDN URL that sends CORS headers, remove the redirect, or route the request through a same-origin proxy. See the configuration reference and the project’s documented CORS issue history.

Make fonts deterministic

Wait for fonts before capturing, use a fallback font in the clone, and inline important SVG text styles:

await document.fonts.ready;

const canvas = await html2canvas(node, {
  onclone: clonedDocument => {
    const svg = clonedDocument.querySelector('svg');
    if (svg) svg.style.fontFamily = 'Arial, sans-serif';
  },
  logging: true
});

This changes only the cloned document. It lets you keep the live page’s typography while making the capture path predictable.

Fix cloned-document and animation problems

The cloned document may differ from the live document because resources are still loading, animations are at another frame, or a selector depends on runtime state. Use onclone to remove animation, replace an external asset, or add a capture-only style.

const canvas = await html2canvas(node, {
  onclone: clonedDocument => {
    clonedDocument.querySelectorAll('*').forEach(el => {
      el.style.animation = 'none';
      el.style.transition = 'none';
    });
    const svg = clonedDocument.querySelector('svg');
    if (svg) {
      svg.style.fontFamily = 'Arial, sans-serif';
      svg.style.overflow = 'visible';
    }
  },
  onError: error => console.warn('capture resource failed', error)
});

Viewport, clipping, and canvas limits

If the SVG is clipped or media-query styles are wrong, set the rendering viewport explicitly:

const canvas = await html2canvas(node, {
  windowWidth: node.scrollWidth,
  windowHeight: node.scrollHeight,
  width: node.scrollWidth,
  height: node.scrollHeight,
  x: 0,
  y: 0,
  logging: true
});

Very large outputs can exceed browser canvas dimensions. Capture a smaller region, reduce scale, split a long page into sections, or export a server-side screenshot when the required dimensions exceed the browser’s limits.

Common errors and fixes

Symptom Likely cause Fix
SVG is completely blank Unsupported feature, failed resource, foreignObject issue, or canvas limit Use the minimal fixture, enable logging, inspect Network errors, test foreignObjectRendering, and reduce dimensions.
Paths render but images do not Cross-origin image, missing CORS header, or redirect to a CDN Use a final URL with CORS, configure useCORS, or use a same-origin proxy.
Text uses the wrong font Font was not loaded in the clone or is cross-origin Await document.fonts.ready, provide a fallback in onclone, and verify font responses.
Colors or styles differ Unsupported CSS, external stylesheet, CSS variable, or foreignObject difference Inline critical SVG styles, remove one feature at a time, and compare default and foreignObject renderers.
SecurityError from toDataURL Canvas was tainted by a cross-origin resource Fix response headers or proxy the resource. allowTaint permits drawing but does not make the canvas readable.
Output is clipped Viewport or element dimensions do not match capture settings Set windowWidth/windowHeight and explicit width/height; check browser limits.
Works in Chrome, fails in Safari or Firefox Engine differences in SVG, fonts, filters, or foreignObject Keep browser-specific fixtures and choose the simplest cross-engine SVG representation.

Choosing a fix

Approach SVG coverage Cross-origin behavior Browser consistency Complexity
Simplify and inline the SVG Best for basic paths, fills, strokes, and text Strong when resources are same-origin Usually the most predictable Low to medium
CORS headers Preserves external assets Requires control of the asset server and final URL Depends on every response Medium
Same-origin proxy Preserves assets when headers cannot change Moves reads into your origin Usually consistent Medium to high
foreignObjectRendering Can improve CSS fidelity Does not remove CORS requirements Requires careful engine testing Medium
Pre-render to PNG Predictable pixels, no vector scalability Asset pipeline still needs access High after generation Medium

Performance and reliability checklist

  • Capture the smallest element that satisfies the use case.
  • Remove animations and wait for fonts and images before starting.
  • Use a sensible scale; higher resolution increases memory and output size.
  • Reuse a minimal regression fixture for every SVG feature you add.
  • Log resource failures during development and surface them in production telemetry.
  • Keep capture dimensions below browser canvas limits and split very large pages.
  • Cache or pre-render repeated static SVG assets rather than rebuilding them on every interaction.
  • Compare output in Chromium, Firefox, and Safari before promising identical pixels.

Or skip the browser setup

If you need a reliable page image rather than a client-side DOM reconstruction, ScreenshotNeo provides a website screenshot API. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also has an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all 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}`);

There is a free plan with 1,000 shots per month and no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Create a free ScreenshotNeo account.

FAQ

Does converting the SVG to a data URL solve every problem?

No. It can remove one external fetch, but fonts, images, unsupported SVG features, and canvas size limits can still fail.

Should I always set allowTaint: true?

No. It may allow drawing a cross-origin resource, but a tainted canvas cannot safely be read with toDataURL or similar APIs. Fix CORS or use a proxy when you need the pixels.

Why does an inline SVG work while an identical external SVG fails?

An external SVG adds a fetch, origin policy, redirect, and response-header dependency. Inline the asset or make the final response readable from the capture origin.

Is foreignObjectRendering more accurate?

It can preserve more browser CSS in supporting engines, but it introduces its own compatibility failures. Treat it as a targeted experiment and keep a fallback.

Can html2canvas guarantee pixel-identical browser screenshots?

No. It reconstructs the DOM with manually implemented CSS and SVG behavior. Use a browser screenshot service when the requirement is a browser-surface capture.