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
| Feature | SnapDOM | html2canvas |
|---|---|---|
Pseudo-elements (::before / ::after) | Full | Partial |
Web fonts (@font-face) | Embedded | Best-effort |
| Shadow DOM | Yes | No |
| CSS variables | Yes | Partial |
CSS counter() / counters() | Yes | No |
Same-origin <iframe> capture | Yes | Limited |
| Line-clamp / multi-line ellipsis | Yes | No |
| Plugin system | Yes | No |
| Output formats | SVG · PNG · JPG · WebP · Canvas · Blob | Canvas → PNG/JPG |
| Runtime dependencies | 0 | 0 |
| Approach | Clone + foreignObject (browser renders) | Re-implements CSS painter |
| License | MIT | MIT |
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:
| html2canvas | SnapDOM |
|---|---|
| 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.
Ready to switch?
Drop SnapDOM into your project, capture your first element, and decide for yourself.
Open the demo Install from npm