Render HTML into 2D canvas

The HTML-in-Canvas API lets you render interactive, accessible DOM subtrees directly into a 2D canvas context. Read the HTML-in-Canvas overview to learn more about the API and its benefits.

This guide demonstrates how to render HTML into a 2D canvas. Rendering HTML in a 2D canvas requires the following steps:

  1. Prepare your canvas. Declaratively set the right attributes to let the browser know it should render HTML into a canvas. This setup is identical across 2D, WebGL, and WebGPU contexts. Learn how to set your canvas up and how to declare HTML elements to be drawn independently.
  2. Draw the element image into the canvas.
  3. [Optional] Synchronize the DOM and the pixels.

Draw HTML elements into 2D canvas

Once the canvas is ready and elements are marked as drawable, render them into the 2D context using the drawElementImage() method.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const childElement1 = document.getElementById("childElement1");
const childElement2 = document.getElementById("childElement2");

canvas.onpaint = () => {
  ctx.reset();
  // 1. Draw the primary element at coordinate (20, 20)
  ctx.drawElementImage(childElement1, 20, 20);
  // 2. Draw the secondary nested button at a custom offset
  ctx.drawElementImage(childElement2, 40, 90);
}

drawElementImage() supports 3-argument, 5-argument, 7-argument, and 9-argument forms and accepts an optional options parameter:

// 1. Basic position: (element, dx, dy, options?)
ctx.drawElementImage(targetElement, 100, 150);

// 2. Scaled / resized: (element, dx, dy, dWidth, dHeight, options?)
ctx.drawElementImage(targetElement, 100, 150, 180, 60);

// 3. Cropped, default size: (element, sx, sy, swidth, sheight, dx, dy, options?)
ctx.drawElementImage(targetElement, 0, 0, 90, 30, 100, 150);

// 4. Cropped and resized: (element, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, options?)
ctx.drawElementImage(targetElement, 0, 0, 90, 30, 100, 150, 180, 60);
Overload form Parameters Layout effect
Default size (element, dx, dy) The whole element at its default size, with the top-left corner at (dx, dy).
Custom destination box (element, dx, dy, dwidth, dheight) Scales the full element image into a dw × dh viewport on canvas.
Cropped, default size (element, sx, sy, swidth, sheight, dx, dy) Only the swidth × sheight area starting at (sx, sy), drawn at that area's default size (the same scale as the default-size form).
Source slice to destination (element, sx, sy, swidth, sheight, dx, dy, dwidth, dheight) Crops the source element starting at (sx, sy) with dimensions swidth × sheight, then maps it to destination (dx, dy, dwidth, dheight).

Synchronize the DOM and the pixels

Because <canvas> rendering is imperative while DOM layout and CSS styling are reactive, the browser needs a mechanism to keep the rendered canvas pixels, interactive hit-test regions, and accessibility trees synchronized. Before learning how to synchronize the DOM and the pixels, make sure you understand the paint event that triggers when DOM elements update and need to be redrawn.

In 2D canvas contexts, the browser automatically synchronizes the element's layout coordinates, accessibility bounds, and pointer hit-testing with the drawn canvas coordinates.

Manually update element geometry

In advanced scenarios, you can opt out of automatic geometry synchronization and control spatial transforms manually. Pass { preserveElementGeometry: true } to drawElementImage() to prevent the browser from automatically overriding the element's existing hit-test coordinates:

// 1. Draw the element without altering its existing hit-test bounds
ctx.drawElementImage(targetElement, elementX, 200, { preserveElementGeometry: true });

// 2. Construct a custom transform matrix as needed,
// assuming scaleX/scaleY = canvas grid pixels per CSS pixel,
// from your resize observer (see "Units and sizing").
const currentTransform = new DOMMatrix();
currentTransform.a = 1.5;          // scale X
currentTransform.e = x / scaleX;   // translate X: grid px → CSS px
currentTransform.f = y / scaleY;   // translate Y: grid px → CSS px

// 3. Explicitly apply the custom transform
canvas.updateElementGeometry(targetElement, { canvasTransform: currentTransform });
In 2D, drawElementImage() updates this geometry automatically. This demo shows how geometry gets out of sync when { preserveElementGeometry: true } is set.

Next steps