Migrate from html2canvas to SnapDOM
html2canvas 1.4.1 shipped in January 2022 and nothing has shipped since. This page maps every option you are using to its SnapDOM equivalent, shows the return value change, and lists the mistakes people make right after switching.
html2canvas(el, { scale: 2 }) becomes snapdom.toCanvas(el, { dpr: 2 }). Most apps change three lines: the import, the call, and what they do with the result.
What changes, in one block
Install @zumer/snapdom and replace the call. The canvas consumer is the closest match to what you have:
// Before
import html2canvas from 'html2canvas'
const canvas = await html2canvas(el, { scale: 2, useCORS: true })
const dataURL = canvas.toDataURL('image/png')
// After
import { snapdom } from '@zumer/snapdom'
const canvas = await snapdom.toCanvas(el, { dpr: 2 })
const dataURL = canvas.toDataURL('image/png')If the canvas only existed to get a PNG, skip it: const img = await snapdom.toPng(el) returns an HTMLImageElement whose src is the data URL.
The return value is a capture, not a canvas
html2canvas resolves to a canvas and leaves the encoding to you. snapdom(el) resolves to a capture object that already holds the frozen SVG and exports it on demand, so one capture can feed several outputs without re-walking the DOM:
const capture = await snapdom(el, { dpr: 2 })
const img = await capture.toPng() // HTMLImageElement
const canvas = await capture.toCanvas() // HTMLCanvasElement
const blob = await capture.toBlob({ format: 'png' }) // Blob for upload
await capture.download({ format: 'jpg', filename: 'card' }) // file to diskThe shortcuts snapdom.toPng, snapdom.toJpg, snapdom.toWebp, snapdom.toSvg, snapdom.toCanvas and snapdom.toBlob do the capture and one export in a single call, which is what most html2canvas call sites want.
Option by option
Every html2canvas option you may have set, and what to write instead. Options not listed here (removeContainer, windowWidth, windowHeight, scrollX, scrollY, letterRendering) have no equivalent because SnapDOM captures the element's own box and never scrolls the page.
| html2canvas | SnapDOM | Notes |
|---|---|---|
scale: 2 | dpr: 2 | html2canvas scale is a pixel density. SnapDOM also has a scale option, but it multiplies the output size in CSS pixels. Writing scale: 2 out of habit gives you a capture twice as large, not twice as sharp. |
backgroundColor: '#fff' | backgroundColor: '#fff' | Same name. The default differs: html2canvas fills white, SnapDOM keeps PNG and SVG transparent and only fills white for JPEG and WebP. A PNG that used to come out white needs the option. |
useCORS: trueallowTaint: trueproxy: url | nothing, or useProxy: base | SnapDOM fetches and inlines images itself. An image served without CORS headers still cannot be read; pass useProxy with the base URL of a proxy for those. There is no tainted mode to opt into. |
ignoreElements: (el) => bool | exclude: [...] or filter: fn | exclude takes selectors and predicates; a predicate that returns true drops the node. filter is the inverse: true keeps. By default an excluded node leaves an invisible box so layout holds; excludeMode: 'remove' collapses it. A data-capture="exclude" attribute works without any option. |
onclone: (doc, el) => {} | a plugin with afterClone | The clone is handed to plugin hooks. An inline plugin needs a name and edits ctx.clone; see the example below. |
width, height | width, height | Output size in pixels. Setting one keeps the aspect ratio. |
x, y | clip: { x, y, width, height } | A rectangle in page coordinates. clip: 'viewport' captures what is on screen. |
canvas: existing | canvas: existing | Same: reuse a canvas you already own as the raster target. |
logging: true | debug: true | With it off, SnapDOM logs nothing. |
imageTimeout | none | A resource that fails shows a placeholder (placeholders: true by default) or the fallbackURL you provide. |
foreignObjectRendering | none | SnapDOM always renders through a foreignObject; that is the whole method, not a mode. |
Replacing onclone
The onclone callback edited the cloned document before html2canvas drew it. The same work goes in a plugin hook, inline for a one-off or shared through snapdom.use() when several call sites need it:
const img = await snapdom.toPng(el, {
plugins: [{
name: 'strip-controls',
afterClone(ctx) {
ctx.clone.querySelectorAll('.no-capture, button').forEach(n => n.remove())
}
}]
})Two differences from onclone: the hook receives the clone of the element, not a whole document, and beforeClone exists for changes to the live DOM that you undo in afterClone. The plugin API lists every hook.
Fonts come along
html2canvas drew text with whatever fonts the page had loaded. SnapDOM renders the clone inside an SVG image, which is an isolated document, so it embeds the web fonts the element uses on its own (embedFonts: 'auto'). Nothing to configure for Google Fonts or self-hosted fonts. Two cases need a word:
- Icon fonts are matched by name; add yours with
iconFontsif glyphs come out as boxes. - A font stylesheet served without CORS headers cannot be read from any site. Check the console for a
[snapDOM] Network/CORS issueline before suspecting the capture.
What renders now that did not before
html2canvas reimplements CSS painting, so its own documentation lists what it skips: box-shadow, filter, mix-blend-mode, object-fit, border-image, background-blend-mode, box-decoration-break, writing-mode, repeating-linear-gradient() and zoom. SnapDOM lets the browser paint the styled clone, so those render as they do on screen, along with pseudo-elements, open shadow roots, typed form values and same-origin iframes.
That is why a migrated capture should be checked against the live element, not against the old html2canvas output: differences are usually things html2canvas was leaving out. The head to head page runs both libraries on the same element so you can see it.
Four mistakes right after switching
- Keeping
scale: 2. You get a capture at twice the CSS size. Usedpr: 2for sharpness; usescaleonly when you want a bigger image. - Expecting a canvas from
snapdom(el). It returns a capture object. Callsnapdom.toCanvas(el), orcapture.toCanvas(). toBlob()with no format. The default export is SVG, so the blob is an SVG blob. Pass{ format: 'png' }when the upload expects a PNG.- Importing it on the server. SnapDOM runs in the browser only. In Next.js, the dynamic import with
ssr: falsehas to live in a client component, as the Next.js guide shows.
Frequently asked questions
Do I still need a CORS proxy?
Only for images served without CORS headers. SnapDOM fetches and inlines images itself; useProxy takes a proxy base URL for the ones that need it.
Will the output match my html2canvas output pixel for pixel?
No. SnapDOM lets the browser paint the styled clone, so properties html2canvas skips now render. Compare against the live element, not against the old capture.
Can both libraries coexist while I migrate?
Yes. They are separate packages with no shared state, so you can move one call site at a time and remove html2canvas when the last one is gone.
Capture, modify and convert any element
Install SnapDOM and export the DOM to PNG, SVG, PDF or any other format.
Open the demoInstallation