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.
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 is now extended. 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 for
more detail):
// 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 copyElementImageToTextureparameters into sourceDict and
destDict objects (see GPUWeb PR#6250):
// 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:
// 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
texElementImage2DtotexElementSubImage2D.` - The method signature changes; most significantly, it adds
xoffsetandyoffsetarguments 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.
// 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 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:
<!-- 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:
// 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():
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:
// ============================================================================
// 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:
// ============================================================================
// 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:
// ============================================================================
// 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:
// ============================================================================
// 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 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 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. Join the developer newsletter to stay up-to-date with any HTML-in-Canvas news.