SnapDOMGitHub★ 8K
Live demo

Live element

Hard parts

transform + shadow

Result

Nothing captured yet

Edit the title, then run. This uses the real library from unpkg.
Migration guide

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.

TL;DR

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 disk

The 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.

html2canvasSnapDOMNotes
scale: 2dpr: 2html2canvas 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: true
allowTaint: true
proxy: url
nothing, or useProxy: baseSnapDOM 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) => boolexclude: [...] or filter: fnexclude 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 afterCloneThe clone is handed to plugin hooks. An inline plugin needs a name and edits ctx.clone; see the example below.
width, heightwidth, heightOutput size in pixels. Setting one keeps the aspect ratio.
x, yclip: { x, y, width, height }A rectangle in page coordinates. clip: 'viewport' captures what is on screen.
canvas: existingcanvas: existingSame: reuse a canvas you already own as the raster target.
logging: truedebug: trueWith it off, SnapDOM logs nothing.
imageTimeoutnoneA resource that fails shows a placeholder (placeholders: true by default) or the fallbackURL you provide.
foreignObjectRenderingnoneSnapDOM 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 iconFonts if 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 issue line 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

  1. Keeping scale: 2. You get a capture at twice the CSS size. Use dpr: 2 for sharpness; use scale only when you want a bigger image.
  2. Expecting a canvas from snapdom(el). It returns a capture object. Call snapdom.toCanvas(el), or capture.toCanvas().
  3. 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.
  4. Importing it on the server. SnapDOM runs in the browser only. In Next.js, the dynamic import with ssr: false has 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