Google announced HTML-in-Canvas at Google I/O 2026 — a new web platform API that lets developers render real live DOM elements directly onto an HTML Canvas. Here's everything you need to know, from core primitives to production-ready patterns.
For over a decade, web developers have faced an uncomfortable tradeoff: build in the DOM
(rich interactivity, accessibility, styling) or build on Canvas (raw pixel control,
high performance, 3D graphics). You couldn't have both — at least not elegantly. Solutions like
html2canvas and rasterizeHTML approximated screenshots server-side or in
the browser, but they were slow, inaccurate, and couldn't handle dynamic content.
At Google I/O 2026 (May 19), Google announced HTML-in-Canvas — a new web platform API that finally bridges this gap. It lets you render real, live DOM elements directly onto an HTML Canvas context, supporting 2D, WebGL, and WebGPU rendering pipelines. Combined with the other 15 Chrome updates announced at I/O, this is one of the most significant additions to the web graphics stack in years. For a full summary of all Chrome announcements, see our Google I/O 2026 Day 2 Recap: Chrome Updates guide.
The API is currently available as an Origin Trial in Chrome 148-150 (stable
channel, May 2026). Developers can opt in via the origin trial token or enable
chrome://flags/#canvas-draw-element. This guide covers every API primitive with
working code examples, browser support timelines, performance strategies, and real-world use cases.
Before HTML-in-Canvas, if you wanted rich interactive HTML — styled forms, complex layouts, live data visualizations — inside a Canvas-based application (game, design tool, video editor), you had three bad options:
html2canvas or
SVG foreignObject. Slow, produces static images, doesn't update when the DOM changes
without re-rendering entirely.The HTML-in-Canvas API solves all of this by giving you a direct pipe from the DOM renderer into your canvas context. It's not a screenshot — it's the actual browser rendering engine painting into your canvas at the pixel level.
The API consists of several primitives that work together. All methods live on the
HTMLCanvasElement and CanvasRenderingContext2D / WebGLRenderingContext
/ GPUCanvasContext interfaces.
Before you can render a DOM element onto a canvas, the browser needs to compute its layout.
layoutSubtree() performs a synchronous layout of the element's subtree, ensuring
all dimensions, positions, and computed styles are ready. This is a mandatory first step
— calling drawElementImage() or texElementImage2D() without first
calling layoutSubtree() produces an empty result.
Paint a DOM element onto a 2D canvas context. The signature mirrors
drawImage() — you can specify destination coordinates and optionally source
cropping. The element is rendered at its natural DOM size and painted onto the canvas at
the specified coordinates.
Upload a DOM element directly as a WebGL or WebGPU texture. This is the most efficient path for 3D rendering — it avoids a CPU round-trip by going directly from the browser compositor to the GPU texture memory.
Copy a rendered DOM element into an existing WebGL texture object. Useful for frameworks like Three.js where you manage texture objects directly.
Return a static ImageBitmap or HTMLImageElement of the rendered
element. This is useful for one-time captures where you don't need live updates.
Returns a DOMMatrix representing the element's current computed transform
relative to the canvas coordinate system. Essential for mapping between Canvas coordinates
and DOM coordinates, and for applying transforms in 3D scenes.
The paint event fires on the canvas when the HTML-in-Canvas render completes.
Listen for it to know when your element has been painted and is ready to use:
canvas.addEventListener('paint', () => {
// Element has been painted — safe to read back, composite, etc.
});
An ElementImage object represents a cached, renderable snapshot of a DOM element.
Create one with canvas.createElementImage(element) for efficient re-rendering
without re-laying out the DOM on every frame.
The simplest case: take a styled HTML form element and paint it onto a 2D canvas. This is useful for game UIs, interactive kiosks, and creative tools:
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const form = document.getElementById('loginForm');
// Step 1: Ensure layout is computed
canvas.layoutSubtree(form);
// Step 2: Draw the element onto the canvas
drawElementImage(form, 50, 50);
// Listen for paint completion
canvas.addEventListener('paint', () => {
console.log('Form rendered on canvas!');
}, { once: true });
For real-time graphics applications, use texElementImage2D() to bind a DOM
element directly as a WebGL texture:
const gl = canvas.getContext('webgl');
const uiElement = document.getElementById('hud-panel');
// Lay out the element
canvas.layoutSubtree(uiElement);
// Upload directly as texture unit 0
texElementImage2D(uiElement, 0);
// Now available as TEXTURE0 in your shader
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, myTexture);
// The HUD element is now a texture your shader can sample
For Three.js developers, the API integrates naturally. Create an offscreen element, lay it out, and use it as a material map:
import * as THREE from 'three';
// Create offscreen HTML content
const div = document.createElement('div');
div.innerHTML = '<h2>Hello, 3D!</h2><p>Rendered via HTML-in-Canvas</p>';
div.style.cssText = 'width:300px;height:200px;background:#1a1a2e;color:white;padding:20px;';
document.body.appendChild(div);
// Lay out and capture
canvas.layoutSubtree(div);
const imageBitmap = captureElementImage(div);
// Create Three.js texture
const texture = new THREE.CanvasTexture(imageBitmap);
// Apply to a material
const material = new THREE.MeshStandardMaterial({ map: texture });
const mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), material);
scene.add(mesh);
When rendering DOM elements in a 3D scene or transformed canvas context, you need to map
coordinates precisely. getElementTransform() returns the full transform matrix:
const transform = canvas.getElementTransform(myElement);
// Returns a DOMMatrix
// Apply transform to a Three.js object
const matrix = new THREE.Matrix4();
matrix.fromArray(transform.toFloat64Array());
threeObject.applyMatrix4(matrix);
// Or use it for hit-testing canvas coordinates back to DOM:
canvas.addEventListener('click', (e) => {
const inverse = transform.inverse();
const domPoint = inverse.transformPoint(
new DOMPoint(e.offsetX, e.offsetY)
);
// Check if domPoint.x / domPoint.y falls within the element bounds
});
Announced alongside HTML-in-Canvas at Google I/O 2026, element-scoped view transitions enable smooth animated transitions between DOM states when those elements are rendered onto a Canvas. This is a critical addition — without it, changing a DOM element's content would cause a jarring visual cut on the Canvas render.
The API works by capturing the previous and next paint states of an element and interpolating between them:
// Start a view transition scoped to the element
const transition = document.startViewTransition({
update: () => {
// Change the DOM content
updateElementContent();
// Re-layout and re-paint for the canvas
canvas.layoutSubtree(myElement);
drawElementImage(myElement, 0, 0);
}
});
await transition.finished;
console.log('Smooth Canvas transition complete!');
The HTML-in-Canvas API has been in development for over a year. Here's the current status:
chrome://flags/#canvas-draw-element. WICG proposal reaches 3.3k+ stars.layoutSubtree, drawElementImage, texElementImage2D,
captureElementImage, getElementTransform. Works on Desktop Chrome.Important: No other browser engine (Firefox, Safari) has committed to implementing HTML-in-Canvas. This is a Chrome-exclusive API for the foreseeable future. Other WebGPU and WebGL innovations continue to be cross-browser — for a broader look at the web AI landscape, see my Chrome Prompt API Guide.
Getting good performance with HTML-in-Canvas requires understanding where the bottlenecks are:
layoutSubtree() is the most expensive operation — it triggers a full browser
layout pass on the element subtree. Call it only when the source DOM actually changes.
For animations at 60fps where the DOM stays the same, call it once and use the cached
ElementImage for subsequent frames.
Don't read back canvas pixels or composite immediately after calling drawElementImage().
Wait for the paint event to fire, which signals that the rendering pipeline
has completed compositing the element onto the canvas.
The HTML-in-Canvas API renders at the canvas's current pixel dimensions, not CSS dimensions. On high-DPI displays (Retina), you must scale your canvas backing store:
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
ctx.scale(dpr, dpr);
// Now drawElementImage() renders at native resolution
Always check for API support before using it:
const supportsHtmlInCanvas =
'layoutSubtree' in HTMLCanvasElement.prototype &&
'drawElementImage' in CanvasRenderingContext2D.prototype;
if (supportsHtmlInCanvas) {
// Use the API
} else {
// Fall back to traditional Canvas rendering or overlaid DOM
}
If you're rendering the same element every frame (e.g., a HUD overlay in a game), create
an ElementImage once and reuse it. This avoids re-laying-out the DOM every frame:
const elementImage = canvas.createElementImage(hudElement);
canvas.layoutSubtree(hudElement);
function renderFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
elementImage.draw(ctx, 10, 10); // Fast path — no DOM layout
requestAnimationFrame(renderFrame);
}
The HTML-in-Canvas API opens up several categories of applications that were previously impractical:
Game engines embedded in the browser (PlayCanvas, Unity WebGL, custom engines) need rich menus, inventory screens, chat panels, and HUD overlays. Instead of reimplementing every UI widget on Canvas, developers can now build UIs in standard HTML/CSS and render them into the game world. The Three.js integration path makes this especially seamless for WebGL-based game engines.
Data viz tools often mix Canvas-rendered charts and graphs with DOM-based tooltips, legends, and controls. With HTML-in-Canvas, tooltips and annotation labels can be true HTML elements rendered directly on the Canvas, preserving their styling and layout while being part of the graphic.
Applications like Figma, Photopea, and Excalidraw render everything on Canvas for performance but need rich text editing, styled inputs, and complex form controls. HTML-in-Canvas allows these tools to embed real DOM elements (text editors, color pickers, dropdowns) at specified Canvas coordinates without the overlay-style positioning hacks.
Canvas-based video editors can composite DOM elements (titling cards, lower thirds, credits rolls) directly into their rendering pipeline, using WebGL textures for GPU-accelerated compositing. The element-scoped view transitions enable professional-grade keyframe animations on DOM-based overlay elements.
Presentation frameworks (like Reveal.js alternatives on Canvas) can embed real HTML content — code blocks with syntax highlighting, iframes, complex tables — directly into Canvas-based slides without the "jumpy overlay" feel.
Artists using Canvas for generative work can now mix DOM-rendered typography, SVG-based elements, and styled HTML widgets into their real-time graphics pipeline, combining the expressive power of Canvas with the layout capabilities of HTML/CSS.
Both texElementImage2D() and future WebGPU equivalents take different paths:
texElementImage2D() to upload directly as a
WebGL texture. Works on all WebGL-capable browsers (Chrome, Firefox, Safari). The API is
synchronous — the texture is ready immediately after the call.copyElementImageToTexture() with a GPUTexture target. Benefits from
WebGPU's lower driver overhead and explicit resource management.
For most applications in 2026, the WebGL path via texElementImage2D() is the
practical choice. WebGPU support is expected to follow in later origin trial iterations.
If you have an existing Canvas-based application and want to adopt HTML-in-Canvas, here's a practical migration plan:
position:absolute;left:-9999px or a detached
div).ctx.fillText() or custom
button drawing, use layoutSubtree() + drawElementImage().getElementTransform() to
handle Canvas ↔ DOM coordinate translation for interactive elements.ElementImage objects and reuse them in the render loop.As with any origin-trial API, there are important limitations to be aware of before building production applications:
drawElementImage() but the actual painting happens asynchronously. The
paint event is your only reliable synchronization signal.layoutSubtree() call creates internal
render surfaces. Call it judiciously and destroy ElementImage objects when they're no longer
needed.layoutSubtree() to prepare an element, then drawElementImage() (2D) or texElementImage2D() (WebGL/WebGPU) to paint it onto your canvas context. The rendered content stays interactive at the source DOM level.chrome://flags/#canvas-draw-element or joining the Origin Trial.drawElementImage() renders an element onto a 2D CanvasRenderingContext2D — it produces a raster image of the element at the current paint time. texElementImage2D() uploads the rendered element directly as a WebGL/WebGPU texture, which is far more efficient for 3D rendering pipelines because it avoids a CPU→GPU copy. For Three.js, copyElementImageToTexture() gives you a ready-to-use WebGL texture handle.THREE.HTMLTexture from an offscreen element and apply it as a material map to any 3D object. This lets you wrap real HTML UIs around 3D geometries, embed rich forms and charts in 3D scenes, and animate HTML content in 3D space — all without prerendering or screenshot hacks.layoutSubtree() is the bottleneck — it triggers a full layout of the subtree. For real-time apps like games, call layoutSubtree() only when the source DOM changes and use the paint event to throttle re-renders. Use captureElementImage() for one-time captures and texElementImage2D() for frame-by-frame textures. Always call drawElementImage() at the correct DPR scale to avoid blurry output. At 60fps with simple elements, overhead is minimal.getElementTransform() and forward pointer events as needed. This is an important architecture consideration for interactive applications.If you're building a Canvas-based application and need rich HTML UIs inside it, yes, but with caveats. The Origin Trial gives you the opportunity to experiment and provide feedback, and the API is stable enough for prototyping and non-critical UIs. For production applications targeting a broad audience, wait for stable release and verify browser support metrics. Always implement graceful degradation — your Canvas app should still work without HTML-in-Canvas, just with less polished UI.
The combination of HTML-in-Canvas, element-scoped view transitions, and the broader Chrome web platform updates (see our WebMCP guide for the AI-agent side of the platform) makes this an exciting time for web graphics. The gap between "what you can build with the DOM" and "what you can build on Canvas" is finally closing.
I'm a full-stack web developer specializing in Canvas, WebGL, and high-performance web applications. If you're planning to adopt HTML-in-Canvas or need help with your Canvas-based project, let's talk.