Library Comparison
zumerlab/snapdom

SnapDOM vs html2canvas

A side-by-side look at the two most-used DOM-to-image libraries. Where each one wins, where each one breaks, and how to switch in five minutes.

Feature matrix Migration snippet
TL;DR

html2canvas is the long-standing default and works fine for plain layouts. SnapDOM is the better choice when you need pseudo-elements, web fonts, Shadow DOM, CSS variables or a plugin pipeline — and when you care about bundle size and speed. Both are MIT-licensed and run client-side.

A quick overview

html2canvas works by reading the DOM and re-painting every element onto a <canvas> using its own CSS engine. It has been around since 2012, has wide adoption, and a long backlog of edge cases that come from re-implementing the browser's renderer.

SnapDOM takes a different approach: it deep-clones the DOM, inlines the styles, fonts and images, and wraps the clone in an SVG <foreignObject>. The browser does the actual rendering, which means anything the browser can paint, SnapDOM can capture — including pseudo-elements, CSS variables, custom counters and Shadow DOM.

Feature matrix

FeatureSnapDOMhtml2canvas
Pseudo-elements (::before / ::after)FullPartial
Web fonts (@font-face)EmbeddedBest-effort
Shadow DOMYesNo
CSS variablesYesPartial
CSS counter() / counters()YesNo
Same-origin <iframe> captureYesLimited
Line-clamp / multi-line ellipsisYesNo
Plugin systemYesNo
Output formatsSVG · PNG · JPG · WebP · Canvas · BlobCanvas → PNG/JPG
Runtime dependencies00
ApproachClone + foreignObject (browser renders)Re-implements CSS painter
LicenseMITMIT

Run the live benchmark suite locally with npm run test:benchmark to reproduce the numbers on your hardware — performance varies a lot depending on element complexity, fonts and image content.

When to use which

Choose SnapDOM when…

  • You use modern CSS: variables, ::before/::after, gradients, transforms, line-clamp.
  • Your UI uses Web Components or Shadow DOM.
  • You need to embed custom fonts in the output.
  • You want to pipe the capture through plugins (filters, watermarks, OCR-friendly output, ASCII, PDF…).
  • You care about bundle size and zero dependencies.

html2canvas might still fit when…

  • You're patching an existing project and the migration cost outweighs the gains.
  • Your target page is plain HTML with no pseudo-elements, custom fonts or Shadow DOM.
  • You need a battle-tested community footprint and don't mind the known fidelity gaps.

Same task, side by side

Capturing #card as a PNG and inserting the resulting image into the page:

html2canvas

import html2canvas from 'html2canvas';

const canvas = await html2canvas(document.querySelector('#card'));
const img = new Image();
img.src = canvas.toDataURL('image/png');
document.body.appendChild(img);

SnapDOM

import { snapdom } from '@zumer/snapdom';

const img = await snapdom.toPng(document.querySelector('#card'));
document.body.appendChild(img);

SnapDOM returns the HTMLImageElement ready to be appended — no manual toDataURL, no intermediate canvas. If you do need a canvas, call snapdom.toCanvas(el) instead.

Migration in 5 minutes

The two libraries cover the same surface for the most common operations. Here's the 1:1 mapping:

html2canvasSnapDOM
html2canvas(el)snapdom.toCanvas(el)
canvas.toDataURL('image/png')snapdom.toPng(el)
canvas.toDataURL('image/jpeg', 0.9)snapdom.toJpg(el, { quality: 0.9 })
canvas.toBlob(cb, 'image/png')await snapdom.toBlob(el)
{ scale: 2 }{ scale: 2 }
{ backgroundColor: '#fff' }{ backgroundColor: '#fff' }
{ ignoreElements: el => … }{ exclude: ['.skip'] }
— (not supported){ embedFonts: true }

A typical migration boils down to a one-line change:

// Before
import html2canvas from 'html2canvas';
const canvas = await html2canvas(el, { scale: 2 });

// After
import { snapdom } from '@zumer/snapdom';
const canvas = await snapdom.toCanvas(el, { scale: 2 });

Frequently asked questions

Is SnapDOM a drop-in replacement for html2canvas?

Almost. The API surface is intentionally similar (toPng, toJpg, toBlob, toCanvas) so swapping the import and the call covers most use cases.

The main difference is that snapdom.toPng() returns an HTMLImageElement directly. If your existing code expects a HTMLCanvasElement, use snapdom.toCanvas() instead.

Does SnapDOM render ::before and ::after pseudo-elements?

Yes — and CSS counters too. SnapDOM materialises pseudo-elements as real elements during the clone phase and resolves counter() / counters() values, so they end up in the final image exactly as the browser paints them.

Will my custom @font-face fonts render correctly?

Yes, when you opt in. Pass { embedFonts: true } and SnapDOM will inline the relevant @font-face declarations into the SVG output so the captured image uses your real fonts instead of falling back to system ones.

Can SnapDOM capture content inside Shadow DOM / Web Components?

Yes. The cloner walks open Shadow DOM trees during capture, so Web Components render the same way they do in the live page. This is one of the largest practical gaps when migrating from html2canvas.

How big is SnapDOM?

The core ES module is under 30 KB minified + gzipped, with zero runtime dependencies. Plugins are imported separately so you only pay for what you use.

Does SnapDOM work in React / Vue / Next.js / Svelte?

Yes — it's framework-agnostic. See the dedicated framework guides for React, Vue and Next.js.

Ready to switch?

Drop SnapDOM into your project, capture your first element, and decide for yourself.

Open the demo Install from npm