mrkeyoor.com_
Tue 22 Sept 22:36 UTC
npmWeb Frontendupdated 22 Sept 2026

html-to-image review

html-to-image 1.11.13 captures one live DOM subtree as an SVG data URL, PNG, JPEG, Blob, canvas, or RGBA byte array. It clones the element, copies computed styles and pseudo-elements, fetches fonts and images, places the serialized HTML inside SVG `foreignObject`, then paints that SVG onto a canvas for raster outputs. Version 1.11.13 adds `-webkit-mask` and `-webkit-mask-image` support. Our browser build measured 13.4 KB minified and 5.3 KB gzipped, so the code is small; asset loading and browser rendering rules create most of the operational risk.

Verdict

html-to-image 1.11.13 added 5.3 KB gzipped in our browser build and installed with 0 dependencies and 0 audit findings, which is a small cost for controlled client-side exports. Skip it for server screenshots, hostile cross-origin content, huge documents, or output that must be identical across browsers.

We installed it

Lab card: what happened when we installed html-to-imageScreenshot of html-to-image documentation
Install✓ · 0.7s1 package on disk · 1 MB
ImportESM import works · require() works · CommonJS package
Browser5.3 KBgzipped (13.4 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does html-to-image install cleanly?

Yes. In a fresh container with an empty cache, npm install html-to-image finished in 0.7s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does html-to-image add to a browser bundle?

5.3 KB gzipped (13.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does html-to-image work with both ESM and CommonJS?

Yes. Both import 'html-to-image' and require('html-to-image') worked in Node 22 in our run. The package is published as CommonJS.

Does html-to-image include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

html-to-image or html2canvas: which should you use?

html2canvas: Use it when you want a long-established canvas renderer and can test its CSS subset against your page. html-to-image 1.11.13 added 5.3 KB gzipped in our browser build and installed with 0 dependencies and 0 audit findings, which is a small cost for controlled client-side exports.

When should you not use html-to-image?

The capture must run in Node without a browser DOM. html-to-image depends on document, SVG foreignObject, Image, and Canvas APIs.

API stability4/5html-to-image keeps six conversion functions on the same `(node, options)` promise shape, and version 1.11.13 changes mask copying without replacing that contract. The package has 0 dependencies in our install, so another library cannot silently change its public behavior underneath it. Browser SVG, CSS, font loading, canvas limits, and CORS policy remain outside the API's control, which prevents a top stability score for exact visual output.
Docs4/5The 1.11.13 README demonstrates PNG, JPEG, SVG, Blob, Canvas, pixel data, and React usage. It documents filter propagation, dimensions, quality, cache keys, image placeholders, pixel ratio, font reuse, auto-scaling, style-property selection, tainted canvases, browser support, and data-URI limits. One long README cannot tell you which CSS properties match in each current browser, so teams still need fixtures covering their own cards and assets.
Maintenance3/5Version 1.11.13 was published on February 14, 2025, and fixed WebKit mask capture. GitHub shows a later push on May 28, 2026, 7,222 stars, an unarchived repository, and 156 open issues after pull requests are excluded. Source activity continues, but the package release is more than a year old and the issue backlog is large for a narrow rendering utility, especially when browser changes create fresh edge cases.
Ecosystem4/5The npm downloads endpoint counted 6,242,342 downloads for August 18 through August 24, 2026, and GitHub reports 7,222 stars. Our install found bundled types, working CommonJS and ESM loading, 0 runtime dependencies, and a 5.3 KB gzipped browser build. Any UI framework can supply an element reference, but the package deliberately stops before storage, download management, PDF pagination, browser automation, or server rendering.

Use it if

  • A user needs to export a controlled share card, chart wrapper, receipt, or other moderate DOM subtree from the browser.
  • The same capture path must return SVG, PNG, JPEG, Blob, Canvas, or pixel data depending on the caller.
  • You can serve fonts and images from the same origin or with CORS headers that permit browser fetching.
  • Export-only dimensions, styles, element filtering, and a fixed pixel ratio cover the required visual adjustments.
Skip it if

Setup reality

We installed html-to-image 1.11.13 in a fresh Node 22 Bookworm sandbox. npm completed in 0.7 seconds, left 1 package using 1 MB on disk, and found 0 known vulnerabilities. The package has 0 direct and 0 peer dependencies, bundled TypeScript declarations, and a 520 KB unpacked size. It is CommonJS without an exports map; both require() and ESM import worked. Our full browser import built to 13.4 KB minified and 5.3 KB gzipped.

There are no credentials or config files. Capture after the target's fonts and images finish loading. The library fetches @font-face URLs, <img> sources, and CSS background images before serialization, so remote assets need CORS headers. imagePlaceholder fills failed image requests, while cacheBust appends a changing query and can turn one export into fresh network traffic.

Output dimensions depend on the element, width and height, canvas sizing options, and device pixel ratio. Set pixelRatio: 1 when a stable 1x result matters. The default uses the device ratio, which can multiply canvas memory on high-density screens. Very large DOM trees can still exceed browser canvas or data-URI limits even with automatic scaling enabled.

Repeated exports should call getFontEmbedCSS() once and pass its result as fontEmbedCSS when every card uses the same fonts. A filter removes a node and all descendants and never receives the root. Catch rejected promises, block duplicate export clicks, and revoke object URLs created from Blobs. Version 1.11.13 still relies on foreignObject, so Internet Explorer remains unsupported.

Patterns

Download a node as PNG download-png

import { toPng } from 'html-to-image'

const node = document.querySelector('#share-card')
if (!(node instanceof HTMLElement)) throw new Error('share card missing')
const url = await toPng(node, { cacheBust: true })
const link = document.createElement('a')
link.download = 'share-card.png'
link.href = url
link.click()

`cacheBust: true` adds a changing query to asset requests. Wait for the node's fonts and images before starting the 1.11.13 capture.

Render an opaque JPEG export-jpeg

import { toJpeg } from 'html-to-image'

const url = await toJpeg(node, {
  quality: 0.9,
  backgroundColor: '#ffffff',
  pixelRatio: 2,
})

JPEG has no alpha channel, so set `backgroundColor`. A 2x pixel ratio raises canvas dimensions and memory use.

Preview a captured Blob create-blob

import { toBlob } from 'html-to-image'

const blob = await toBlob(node, { pixelRatio: 1 })
if (!blob) throw new Error('capture produced no blob')
const url = URL.createObjectURL(blob)
preview.src = url
preview.addEventListener('load', () => URL.revokeObjectURL(url), { once: true })

`toBlob` may return `null` when canvas encoding fails. Revoke each object URL after its consumer finishes loading.

Remove marked descendants exclude-private-elements

import { toPng } from 'html-to-image'

const url = await toPng(node, {
  filter: child => !child.classList?.contains('private'),
})

A false filter result removes that element and its complete subtree. The filter callback does not run for the root node.

Override dimensions and clone styles set-export-layout

const url = await toPng(node, {
  width: 1200,
  height: 630,
  backgroundColor: '#fff',
  style: { margin: '0', transform: 'none' },
})

`width` and `height` apply to the cloned node. `canvasWidth` and `canvasHeight` scale the rendered canvas instead.

Embed shared fonts once reuse-font-data

import { getFontEmbedCSS, toSvg } from 'html-to-image'

const fontEmbedCSS = await getFontEmbedCSS(firstCard)
const first = await toSvg(firstCard, { fontEmbedCSS })
const second = await toSvg(secondCard, { fontEmbedCSS })

Reuse `fontEmbedCSS` only when both nodes use the same font rules. It avoids repeating discovery, downloads, and base64 encoding.

Discard unneeded font formats prefer-woff2

const url = await toPng(node, {
  preferredFontFormat: 'woff2',
})

Without `preferredFontFormat`, matching `@font-face` rules can cause every declared source format to be downloaded and embedded.

Fill an image that cannot be fetched fallback-image

const transparent = 'data:image/gif;base64,R0lGODlhAQABAAAAACw='
const url = await toPng(node, { imagePlaceholder: transparent })

`imagePlaceholder` covers a failed fetch. It cannot make a successfully loaded cross-origin canvas safe to read.

Add content after capture draw-on-canvas

import { toCanvas } from 'html-to-image'

const canvas = await toCanvas(node, { pixelRatio: 1 })
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('2D context unavailable')
ctx.fillStyle = '#111827'
ctx.fillText('Exported locally', 16, canvas.height - 16)

Canvas access or later encoding can throw when captured cross-origin content taints the bitmap.

Inspect one RGBA pixel read-pixels

import { toPixelData } from 'html-to-image'

const width = node.scrollWidth
const pixels = await toPixelData(node, { pixelRatio: 1 })
const x = 10
const y = 20
const offset = 4 * (y * width + x)
const rgba = pixels.slice(offset, offset + 4)

Each output pixel occupies 4 bytes in RGBA order. Coordinate math must include the selected pixel ratio.

Export a React ref once capture-react-ref

const cardRef = useRef(null)
const [busy, setBusy] = useState(false)

async function exportCard() {
  if (!cardRef.current || busy) return
  setBusy(true)
  try {
    saveDataUrl(await toPng(cardRef.current))
  } finally {
    setBusy(false)
  }
}

A React ref can be null, and state updates immediately before capture may need a render frame before the DOM clone sees them.

Copy an explicit CSS property set limit-copied-styles

const url = await toPng(node, {
  includeStyleProperties: [
    'background', 'color', 'display', 'font',
    'height', 'margin', 'padding', 'transform', 'width',
  ],
})

Omitted computed properties disappear from the clone. Test this optimization against every component type included in exports.

Alternatives

PackageRegistryPick it when
html2canvasnpmUse it when you want a long-established canvas renderer and can test its CSS subset against your page.
dom-to-image-morenpmUse it when an older dom-to-image integration needs a maintained compatible fork.
modern-screenshotnpmUse it to compare another DOM-cloning implementation on pages that expose html-to-image edge cases.

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.