![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 />

Chrome 150 and 155 introduced several changes to the HTML-in-Canvas API. These
changes responded to community and standards feedback with the aim to improve
the API's ergonomics. This blog post outlines the changes, and how you can ensure
your implementation is up-to-date with them.

> [!NOTE]
> **Note:** The [HTML-in-Canvas API is in Origin Trial](https://developer.chrome.com/origintrials/#/view_trial/3478467762190286849) through Chrome 160. This means the feature is still experimental, and its implementation might change based on feedback. This experimentation lets us refine the API design based on real-world usage before it ships in Chrome stable. [Learn more about Origin Trials](https://developer.chrome.com/docs/web-platform/origin-trials.md.txt), or let us know how we can improve HTML-in-Canvas by [filing an issue on GitHub](https://github.com/WICG/html-in-canvas/issues).

The updates include renaming attributes and methods, requiring explicit texture
memory pre-allocation, and updating DOM synchronization models. Additionally,
the[HTML-in-Canvas Origin Trial](https://developer.chrome.com/origintrials/#/view_trial/3478467762190286849)
is now[extended](https://chromestatus.com/feature/5172548013916160?gate=5139919822520320).
What follows is an explanation of what's changing, why, and how to update your
codebase.

## Changes in Chrome 150

In Chrome 150, we changed argument signatures across WebGL and WebGPU to
eliminate redundancy and align with modern graphics conventions.

### WebGL

We removed redundant arguments (`level`, `srcFormat`, `destType`) from `texElementImage2D`
(see the [WebGL discussion](https://github.com/KhronosGroup/WebGL/pull/3752) for
more detail):

```javascript
// Old implementation (Chrome < 150)
const level = 0;
const internalFormat = gl.RGBA;
const srcFormat = gl.RGBA;
const destType = gl.UNSIGNED_BYTE;

gl.texElementImage2D(
  gl.TEXTURE_2D,
  level,
  internalFormat,
  srcFormat,
  destType,
  element
);

// New implementation (from Chrome 150)
const internalFormat = gl.RGBA8;
gl.texElementImage2D(gl.TEXTURE_2D, internalFormat, element);
```

### WebGPU

We standardized `copyElementImageToTexture`parameters into `sourceDict` and
`destDict` objects (see [GPUWeb PR#6250](https://github.com/gpuweb/gpuweb/pull/6250)):

```javascript
// Old implementation (Chrome < 150)
GPUQueue.copyElementImageToTexture(
  element,
  width,
  height,
  { texture: texture }
);

// New implementation (from Chrome 150)
const sourceDict = { source: element };
const destDict = {
  destination: { texture: texture },
  width: width,
  height: height
};

GPUQueue.copyElementImageToTexture(sourceDict, destDict);
```

## Changes in Chrome 155

Chrome 155 introduces further refinements based on developer feedback. We're
updating method names, requiring explicit texture memory pre-allocation,
supporting complex DOM subtrees, and decoupling DOM synchronization.

### WebGPU

The WebGPU method has been renamed from `copyElementImageToTexture` to
`drawElementImageToTexture`. Along with the rename, parameter coordinates are
expanded (`sourceX`, `sourceWidth`), and destination bounds are grouped into a
standard `size` (`GPUExtent3D`) object:

```javascript
// BEFORE (Chrome 150)
const sourceDict = {
  source: myElement,
  sx: 0,
  sy: 0,
  swidth: 100,
  sheight: 100
};
const destDict = {
  destination: { texture: myTexture },
  width: 300,
  height: 200
};

queue.copyElementImageToTexture(sourceDict, destDict);

// AFTER (Chrome 155+)
const sourceDict = {
  source: myElement,
  sourceX: 0,
  sourceY: 0,
  sourceWidth: 100,
  sourceHeight: 100
};
const destDict = {
  texture: myTexture,
  size: { width: 300, height: 200 }
};

queue.drawElementImageToTexture(sourceDict, destDict);
```

### WebGL

There are three aspects to changes in WebGL for HTML-in-Canvas:

- The method name changes from `texElementImage2D` to `texElementSubImage2D`.\`
- The method signature changes; most significantly, it adds `xoffset` and `yoffset` arguments to optionally allow specifying an offset into the destination texture at which to draw the element.
- The method no longer automatically allocates buffer storage for the destination texture; the storage must be explicitly allocated prior to using the API. This is similar to the behavior of `texSubImage2D`.

```javascript
// 1. Capture natural layout size of the element
const elementImage = canvas.captureElementImage(element);

// 2. Pre-allocate texture backing once (or when element resizes)
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA8,
  Math.ceil(elementImage.width),
  Math.ceil(elementImage.height),
  0,
  gl.RGBA,
  gl.UNSIGNED_BYTE,
  null // null reserves VRAM without uploading pixels
);

// 3. Upload DOM into pre-allocated VRAM
gl.texElementSubImage2D(
  gl.TEXTURE_2D,
  0,                  // level
  0,                  // xoffset
  0,                  // yoffset
  element             // pass DOM element
);
```

### Canvas attributes and drawable child elements

We're updating how you prepare your canvas to render HTML.

#### Renaming `layoutsubtree` to `content="drawable"`

To align with HTML attribute naming conventions, the boolean `layoutsubtree`
attribute on the `<canvas>` element has been renamed to `content="drawable"` (see
[WICG Issue #169](https://github.com/WICG/html-in-canvas/issues/169) for
detailed discussion).

#### The `drawable` attribute for descendants

Previously, only direct children of a canvas were rendered. To support complex
DOM hierarchies without flattening markup, you can now explicitly add the
`drawable` attribute to any descendant element you intend to draw independently:

```javascript
<!-- BEFORE -->
<canvas layoutsubtree>
  <div id="badge">
    <h2>Player Stats</h2>
    <button id="actionBtn">Equip</button>
  </div>
</canvas>

<!-- AFTER (Chrome 155+) -->
<canvas content="drawable">
  <!-- Subtree A (Body and heading) -->
  <div id="badge" drawable>
    <h2>Player Stats</h2>
    <!-- Subtree B (Excluded from Subtree A; drawn and animated independently) -->
    <button id="actionBtn" drawable>Equip</button>
  </div>
</canvas>
```

An element with `drawable` captures its entire subtree *except* for nested
descendants that also specify `drawable`. This allows parent containers and child
elements like buttons to be animated and drawn in separate draw calls.

### Hit-testing synchronization

Previously, developers had to capture the return value from drawing methods and
pipe it into `element.style.transform`. The `drawElementImage()` method now returns
void, and synchronization depends on the context.

#### 2D Contexts: Automatic Synchronization

In 2D canvas, the browser automatically synchronizes DOM hit-test bounds and
screen reader focus rings to the drawn coordinates. You can remove all manual
transform piping:

```javascript
// BEFORE
const transform = ctx.drawElementImage(element, x, y);
element.style.transform = transform.toString();

// AFTER (Chrome 155+)
ctx.drawElementImage(element, x, y);
// No style update needed: position & hit-testing sync automatically
```

To manage geometry manually in 2D, pass `{ preserveElementGeometry: true }` to
`drawElementImage()` and apply transforms using `canvas.updateElementGeometry()`.

#### 3D Contexts: Explicit `updateElementGeometry()`

To synchronize DOM in WebGL or WebGPU contexts, construct a `DOMMatrix` transform
and pass it to `canvas.updateElementGeometry()`:

```javascript
canvas.addEventListener('paint', () => {
  // 1. Upload element snapshot to GPU texture
  device.queue.drawElementImageToTexture({ source: element }, { texture });

  // 2. Render 3D scene...
  drawScene();

  // 3. Construct transform matrix as needed
  const canvasTransform = new DOMMatrix().translate(x, y);

  // 4. Update DOM hit-testing and accessibility bounding boxes
  canvas.updateElementGeometry(element, { canvasTransform });
});
```

## Cross-version compatibility

If your application must support users across multiple Chrome versions during
the transition period, check for the presence of the updated methods on their
prototypes and adjust your implementation accordingly.

Make sure you set correct `<canvas>` element attributes for every browser
version:

```javascript
// ============================================================================
//  Cross-version canvas markup
// ============================================================================
if (!canvas.hasAttribute('content') && !canvas.hasAttribute('layoutsubtree')) {
  if ('content' in HTMLCanvasElement.prototype) {
    // Chrome 155+ (New standardized attribute)
    canvas.setAttribute('content', 'drawable');
  } else {
    // Chrome < 155 (Legacy boolean attribute)
    canvas.setAttribute('layoutsubtree', '');
  }
}
```

For 2D context, inspect the return value of `drawElementImage()` and check
canvas prototype attributes to support both versions:

```javascript
// ============================================================================
// Cross-version 2D drawing & synchronization
// ============================================================================
function draw2DElement(ctx, element, x, y) {
  // Execute the 2D draw call
  const result = ctx.drawElementImage(element, x, y);

  if (result !== undefined && typeof result?.toString === 'function') {
    // Chrome < 155: drawElementImage returned a DOMMatrix
    // Requires manual transform piping to align DOM hit-test bounds:
    element.style.transform = result.toString();
  } else {
    // Chrome 155+: drawElementImage returns void / undefined
    // The browser automatically aligns DOM positioning, clicks, and a11y bounds!
    // If you previously had element.style.transform logic here, clear it:
    if (element.style.transform) {
      element.style.transform = "";
    }
  }
}
```

For WebGPU, use the compatible method signature and name:

```javascript
// ============================================================================
// WebGPU cross-version support
// ============================================================================
if ('drawElementImageToTexture' in GPUQueue.prototype) {
  // Chrome 155+ (Latest implementation)
  queue.drawElementImageToTexture(
    { source: element, sourceX: 0, sourceY: 0, sourceWidth: w, sourceHeight: h },
    { texture: myTexture, size: { width: w, height: h } }
  );
} else if (typeof queue.copyElementImageToTexture === "function") {
  // Chrome 150 - 154: Try the Chrome 150+ dictionary signature first,
  // then fall back to the Chrome < 150 signature
  try {
    queue.copyElementImageToTexture(
      { source: element },
      { destination: { texture: myTexture }, width: w, height: h }
    );
  } catch (err) {
    // Chrome < 150
    queue.copyElementImageToTexture(element, { texture: myTexture });
  }
}
```

Make sure to check browser compatibility for WebGL:

```javascript
// ============================================================================
// WebGL cross-version support
// ============================================================================
if ('texElementSubImage2D' in WebGL2RenderingContext.prototype) {
  // Chrome 155+ (Explicit pre-allocation + sub-image)
  gl.texElementSubImage2D(gl.TEXTURE_2D, 0, 0, 0, element);
} else if (gl.texElementImage2D && gl.texElementImage2D.length === 3) {
  // Chrome 150-154 (3-argument signature)
  gl.texElementImage2D(gl.TEXTURE_2D, gl.RGBA8, element);
} else if (gl.texElementImage2D) {
  // Chrome < 150 (6-argument signature)
  gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, element);
}
```

## Origin Trial extension

The [HTML-in-Canvas Origin Trial](https://developer.chrome.com/origintrials/#/view_trial/3478467762190286849)
has been extended through Chrome 160. Make sure to renew your origin trial token
to continue offering the HTML-in-Canvas feature to your users. See the [Origin Trial article](https://developer.chrome.com/docs/web-platform/origin-trials.md.txt#renew)
for more details.

## Share your feedback

Let us know how these updates work in your applications
by[filing an issue on the WICG GitHub repository](https://github.com/WICG/html-in-canvas/issues).
Join the [developer newsletter](https://groups.google.com/g/html-in-canvas-developer-newsletter)
to stay up-to-date with any HTML-in-Canvas news.