html-to-image
html-to-image turns a live browser DOM node into an SVG data URL, PNG, JPEG, Blob, Canvas, or raw RGBA bytes. It clones the node, copies computed styles and pseudo-elements, embeds fonts and images, serializes the clone inside an SVG foreignObject, then optionally paints that SVG to a canvas. The appeal is a small, dependency-free API for share cards, exports, and visual snapshots when browser rendering is good enough.
A useful small browser utility for controlled, same-origin UI exports. Do not install it as a general screenshot engine for arbitrary pages, huge documents, or exact cross-browser output.
Use it if
- You need a user-triggered PNG or JPEG export of a reasonably sized DOM subtree in a modern browser
- You want to filter private controls, apply export-only styles, or set output dimensions without changing the live node
- You need an SVG data URL, Blob, Canvas, or raw pixels from the same capture flow
- Your captured assets have compatible CORS rules or can be served from the same origin
- You need server-side screenshots or exact browser automation; the implementation depends on a live DOM, SVG foreignObject, Image, and Canvas APIs
- You need pixel-identical output across browsers; the README notes significantly better Chrome performance on large trees and the result depends on each browser's SVG and CSS behavior
- The node contains cross-origin images, fonts, or a tainted canvas without CORS permission; canvas security rules can block the final export
- You export huge or deeply nested DOM trees; the README warns that large captures can fail at browser data-URI limits, and cloning plus style and font embedding is expensive
- You must support Internet Explorer or browsers without SVG foreignObject; the README explicitly says Internet Explorer is not and will not be supported
Setup reality
`npm install html-to-image` is the whole package install: version 1.11.13 has no runtime or peer dependencies and ships TypeScript code and module entry points. The hard part is preparing the page for capture. Call it only after fonts and images have loaded, and use a real HTMLElement reference rather than a selector that may return null. The library fetches and embeds web fonts, `<img>` sources, and CSS background images. Same-origin assets usually work; cross-origin assets need permissive CORS headers, and any tainted canvas can make canvas export fail. `cacheBust` can avoid stale asset responses but also changes requests. A failed image becomes an empty area unless you provide `imagePlaceholder`. Device pixel ratio affects output size and memory; set `pixelRatio: 1` for predictable dimensions or choose an explicit scale. Very large nodes can exceed data-URI or canvas limits even when `skipAutoScale` is false. Filters exclude a node and all of its descendants and are not called for the root. Repeated captures should reuse `getFontEmbedCSS()` through `fontEmbedCSS` to avoid downloading and parsing the same fonts every time. Because the capture is asynchronous, disable duplicate export clicks, catch failures, and revoke any object URLs you create from Blobs. Treat the result as a browser rendering, not a print engine with pagination or CSS fidelity guarantees.
Patterns
Capture a node and download a PNGdownload-png
import { toPng } from 'html-to-image';
const node = document.querySelector('#share-card');
if (!(node instanceof HTMLElement)) throw new Error('share card missing');
const dataUrl = await toPng(node, { cacheBust: true });
const link = document.createElement('a');
link.download = 'share-card.png';
link.href = dataUrl;
link.click();Wait for the node's fonts and images before capturing. `cacheBust` changes asset URLs and can trigger new network requests.
Export a compressed JPEG with a backgroundexport-jpeg
import { toJpeg } from 'html-to-image';
const dataUrl = await toJpeg(node, {
quality: 0.9,
backgroundColor: '#ffffff',
pixelRatio: 2,
});JPEG has no transparency, so set a background color explicitly. Higher pixel ratios increase both sharpness and memory use.
Create and clean up a Blob URLcreate-blob-url
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` can produce null when canvas encoding fails. Revoke object URLs after use to release browser memory.
Exclude controls and private contentfilter-elements
const dataUrl = await toPng(node, {
filter: (child) => !child.classList?.contains('exclude-from-export'),
});Rejecting one node also rejects its descendants, and the filter is not called for the root node.
Apply styles only to the captured cloneapply-export-style
const dataUrl = await toPng(node, {
backgroundColor: 'white',
width: 1200,
height: 630,
style: {
margin: '0',
transform: 'none',
},
});The style object is copied to the cloned root. Width and height change the cloned node, while canvasWidth and canvasHeight scale the full rendered result.
Reuse embedded font CSS across capturesreuse-font-css
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 this only when the captured nodes need the same fonts. It avoids repeating font discovery, downloads, and base64 encoding.
Embed only WOFF2 font sourceschoose-font-format
const dataUrl = await toPng(node, {
preferredFontFormat: 'woff2',
});Without this option, every source format in a matching `@font-face` rule may be downloaded and embedded.
Provide a placeholder for failed imageshandle-missing-images
const transparentPixel = 'data:image/gif;base64,R0lGODlhAQABAAAAACw=';
const dataUrl = await toPng(node, {
imagePlaceholder: transparentPixel,
});The default placeholder is empty, which leaves blank areas. A placeholder does not bypass CORS restrictions for assets that did load into a tainted canvas.
Get a canvas for further processingrender-to-canvas
import { toCanvas } from 'html-to-image';
const canvas = await toCanvas(node, { pixelRatio: 1 });
const context = canvas.getContext('2d');
if (!context) throw new Error('2D canvas unavailable');
context.fillStyle = '#111827';
context.fillText('Generated in browser', 16, canvas.height - 16);Drawing or exporting can still throw if cross-origin content has tainted the canvas.
Read RGBA bytes from the captureinspect-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 pixel occupies four bytes in RGBA order. Keep `pixelRatio: 1` or calculate against the scaled output dimensions.
Capture a React ref after a button clickcapture-react-ref
const cardRef = useRef(null);
const [busy, setBusy] = useState(false);
async function exportCard() {
if (!cardRef.current || busy) return;
setBusy(true);
try {
const url = await toPng(cardRef.current, { cacheBust: true });
saveDataUrl(url);
} finally {
setBusy(false);
}
}Guard a null ref and duplicate clicks. State changes made immediately before capture may need a render frame before the DOM reflects them.
Copy only selected computed styleslimit-style-work
const dataUrl = await toPng(node, {
includeStyleProperties: [
'background', 'color', 'display', 'font', 'height',
'margin', 'padding', 'transform', 'width',
],
});This can speed up large captures, but omitted properties disappear from the clone. Test the final list against every component included in the export.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| html2canvas | npm | You prefer a canvas-oriented renderer with a long history and can accept its own CSS support limits |
| dom-to-image-more | npm | You need a maintained continuation of the older dom-to-image API for an existing integration |
| modern-screenshot | npm | You want another foreignObject-based implementation with newer capture options to compare on difficult pages |