Render HTML to Canvas

There are two ways to render HTML into a <canvas>: a DOM-screenshotting library like html2canvas, which re-implements CSS rendering in JavaScript, or the native drawElementImage() API from the WICG HTML-in-Canvas spec, which has the browser paint real, live DOM elements directly into the canvas.

html2canvas vs drawElementImage() at a glance

html2canvas (library) drawElementImage() (native)
Fidelity Approximation — CSS is re-implemented in JS, so ligatures, subpixel text, some filters, and newer CSS features render differently or not at all The browser's own renderer paints the element — output matches what the page shows, fonts and layout included
Performance Clones the DOM and rasterizes in JavaScript — typically tens to hundreds of milliseconds per capture on real pages Uses the browser's paint pipeline; fast enough to redraw every frame for animation and video capture
Live / interactive content One-shot static snapshot; re-capture for every change Continuous — the paint event fires when a child's rendering changes, and hit testing plus the accessibility tree keep working via layoutsubtree
Browser support All modern browsers Chromium dev trial — Chrome Canary and Brave Stable behind the canvas-draw-element flag; no Firefox or Safari implementation yet
Bundle size ~45 kB min+gzip shipped to every visitor 0 kB — built into the browser

The html2canvas approach — and its limits

html2canvas has been the standard answer for over a decade, and for good reason: it works in every browser today, needs no flags, and captures (almost) any element on the page with one call:

import html2canvas from 'html2canvas';

const target = document.querySelector('#card');
const canvas = await html2canvas(target);
document.body.appendChild(canvas);
// or export it:
const png = canvas.toDataURL('image/png');

Under the hood it does something heroic: it walks the DOM, reads every computed style, and redraws the whole tree onto a canvas using 2D drawing commands — effectively a CSS renderer written in JavaScript. That design is also the source of its limits:

None of this is a knock on the library — it solves a problem the web platform simply had no API for. The HTML-in-Canvas proposal is that missing API.

The native way: layoutsubtree + drawElementImage()

The WICG HTML-in-Canvas spec adds a small API surface to <canvas>: a layoutsubtree attribute that opts the canvas's children into layout, a drawElementImage() context method that paints a child into the canvas, and a paint event that tells you when to redraw. A complete working example:

<canvas id="c" layoutsubtree>
  <div id="content">Hello</div>
</canvas>
<script>
  const ctx = c.getContext('2d');

  // The paint event fires whenever a child element's rendering
  // changes — redraw there, once per frame at most.
  c.onpaint = () => {
    ctx.reset();
    const t = ctx.drawElementImage(content, 0, 0);
    // Sync the DOM position with the drawn position so hit
    // testing and accessibility keep working.
    content.style.transform = t.toString();
  };

  // Size the canvas grid to device pixels to prevent blurriness.
  new ResizeObserver(([e]) => {
    c.width = e.devicePixelContentBoxSize[0].inlineSize;
    c.height = e.devicePixelContentBoxSize[0].blockSize;
  }).observe(c, { box: 'device-pixel-content-box' });
</script>

Key behaviors to know:

Full signatures and IDL are in the API reference. The API is currently a Chromium dev trial: enable chrome://flags/#canvas-draw-element in Chrome Canary or Brave Stable — the browser support page has the two-minute setup.

Migrating from html2canvas

// One-shot export, html2canvas style: force a paint,
// draw, then read the pixels out.
c.addEventListener('paint', () => {
  ctx.reset();
  ctx.drawElementImage(content, 0, 0);
  c.toBlob((blob) => downloadOrUpload(blob), 'image/png');
}, { once: true });
c.requestPaint();

See it running

Three live demos cover the common html2canvas use cases end to end:

Next steps: browse the full gallery of interactive HTML canvas examples, read the spec overview for the full design, or follow the flag setup guide to try the demos in your own browser.