![Natalia Markoborodova](https://web.dev/images/authors/nmarkoborodova.jpg) Natalia Markoborodova [X](https://twitter.com/nmarkoborodova) [GitHub](https://github.com/ewewraw) [LinkedIn](https://www.linkedin.com/in/natalia-markoborodova) [Bluesky](https://bsky.app/profile/nmarkoborodova.bsky.social)

<br />

The HTML-in-Canvas API lets you render interactive, accessible DOM subtrees directly into a 2D canvas context. Read the [HTML-in-Canvas overview](https://developer.chrome.com/docs/web-platform/html-in-canvas.md.txt) 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](https://developer.chrome.com/docs/web-platform/html-in-canvas/concepts.md.txt#prepare) and how to [declare HTML elements to be drawn independently](https://developer.chrome.com/docs/web-platform/html-in-canvas/concepts.md.txt#drawable).
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.

```javascript
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:

```javascript
// 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](https://developer.chrome.com/docs/web-platform/html-in-canvas/concepts.md.txt#paint) 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:

```javascript
// 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

- Understand the [core technical concepts behind HTML-in-Canvas](https://developer.chrome.com/docs/web-platform/html-in-canvas/concepts.md.txt).
- Learn how to [render HTML in a WebGL Canvas](https://developer.chrome.com/docs/web-platform/html-in-canvas/webgl.md.txt).
- Learn how to [render HTML in a WebGPU Canvas](https://developer.chrome.com/docs/web-platform/html-in-canvas/webgpu.md.txt).
- Learn about [fallback strategies](https://developer.chrome.com/docs/web-platform/html-in-canvas/concepts.md.txt#fallback).
- File an [issue on GitHub](https://github.com/WICG/html-in-canvas/issues) to share your feedback.
- Check out the [HTML-in-Canvas demo collection by the community](https://github.com/GoogleChromeLabs/css-web-ui-demos/blob/main/html-in-canvas/awesome-html-in-canvas.md).