# HTML-in-Canvas — Full Documentation > Single-file bundle of every spec doc on https://html-in-canvas.dev. Generated 2026-09-01. > The shorter companion at /llms.txt has the project overview and > demo index; this file contains the full markdown of the spec docs > so an LLM can ingest the whole reference in one request. --- # Spec Overview _Source: https://html-in-canvas.dev/docs/overview/_ # HTML-in-Canvas Spec Overview **HTML-in-Canvas** is a proposed web API (a [WICG explainer](https://github.com/WICG/html-in-canvas)) for drawing real, live DOM elements directly into a `` — rendered by the browser's own engine, accessible, and redrawable every frame. It replaces DOM-screenshotting workarounds like `html2canvas` with three native primitives. The whole API in one taste: ```html
Hello, canvas
``` `layoutsubtree` opts the canvas's children into layout, `drawElementImage()` paints a child into the canvas, and the `paint` event tells you when to redraw. **Where to go next:** - [API Reference](/docs/api-reference/) — full IDL, overloads, and behavior notes - [Browser Support](/docs/browser-support/) — enable `chrome://flags/#canvas-draw-element` in two minutes - [Demo gallery](/demos/) — live examples running in your browser - How to [render HTML to canvas](/render-html-to-canvas/) — the native API vs `html2canvas`, with migration notes ## Background **Source:** https://github.com/WICG/html-in-canvas **Status:** Living explainer, continuously updated. Dev trial behind `chrome://flags/#canvas-draw-element` in Chrome Canary, and also available in recent Brave Stable builds (≥ 1.89.132 / Chromium 147) at `brave://flags/#canvas-draw-element`. **Authors:** Philip Rogers (pdr@chromium.org), Stephen Chenney (Igalia), Chris Harrelson, Philip Jagenstedt, Khushal Sagar, Vladimir Levin, Fernando Serboncini (all Chromium) ## Problem There is no web API to render complex HTML layouts into a ``. Canvas-based content (games, charts, creative tools, 3D scenes) suffers in: - **Accessibility** — canvas fallback content doesn't reliably match what's rendered - **Internationalization** — canvas text APIs can't handle RTL, vertical text, complex scripts - **Performance** — developers resort to `html2canvas`-style hacks (slow, incomplete) - **Quality** — `ctx.fillText()` can't match browser-rendered text with fonts, ligatures, subpixel rendering ## Use Cases 1. **Styled, laid-out content in canvas** — chart labels, rich text boxes in creative tools, in-game menus 2. **Accessibility** — drawn elements ARE the fallback content, so they always match 3. **HTML + WebGL shaders** — apply general GPU effects to HTML elements 4. **HTML in 3D** — render rich 2D content as textures in 3D scenes 5. **Media export** — export HTML content as images or video via canvas ## Solution: Three Primitives + One Helper ### 1. `layoutsubtree` attribute An attribute on `` that opts its direct children into layout and hit testing. ```html
I'm laid out but invisible until drawn
``` Children behave as if visible (participate in layout, hit testing, accessibility tree) but their rendering is NOT visible to the user until explicitly drawn via `drawElementImage()`. Technical effects on direct children: - Creates a stacking context - Becomes a containing block for all descendants - Has paint containment ### 2. `drawElementImage()` (and WebGL/WebGPU equivalents) Draws a direct child of the canvas into the canvas. Returns a `DOMMatrix` transform for synchronization. ```js const transform = ctx.drawElementImage(element, x, y); const transform = ctx.drawElementImage(element, x, y, width, height); const transform = ctx.drawElementImage(element, sx, sy, sw, sh, dx, dy); const transform = ctx.drawElementImage(element, sx, sy, sw, sh, dx, dy, dw, dh); ``` **Key behaviors:** - Canvas CTM is applied when drawing - CSS transforms on the source element are IGNORED for drawing (but still affect hit testing) - Overflow is clipped to the element's border box - If width/height omitted, element maintains its on-screen size proportions - Returns a CSS transform to synchronize DOM position with drawn position **WebGL equivalent:** `gl.texElementImage2D(target, level, internalformat, format, type, element)` **WebGPU equivalent:** `queue.copyElementImageToTexture(element, destination)` ### 3. `paint` event Fires when rendering of any canvas children has changed. Fires just after intersection observer steps during `update-the-rendering`. ```js canvas.onpaint = (event) => { // event.changedElements — array of children whose rendering changed ctx.reset(); const transform = ctx.drawElementImage(myElement, 0, 0); myElement.style.transform = transform.toString(); }; ``` **Key behaviors:** - Contains `changedElements` — list of children that changed - CSS transform changes do NOT trigger paint (transforms are ignored for rendering) - Canvas drawing commands in paint event appear in current frame - DOM changes in paint event appear in NEXT frame - `requestPaint()` forces paint event to fire (like `requestAnimationFrame()`) ### 4. `captureElementImage()` — for OffscreenCanvas/workers ```js const elementImage = canvas.captureElementImage(element); worker.postMessage({ elementImage }, [elementImage]); // transferable ``` Creates a transferable `ElementImage` snapshot for use in workers with `OffscreenCanvas`. ## Synchronization The element's DOM position must match its drawn position for hit testing, accessibility, and intersection observer to work. `drawElementImage()` returns the CSS transform to apply: ```js const transform = ctx.drawElementImage(element, x, y); element.style.transform = transform.toString(); ``` For 3D contexts, use `canvas.getElementTransform(element, drawTransform)`. The general formula: ``` T_origin^-1 * S_css_to_grid^-1 * T_draw * S_css_to_grid * T_origin ``` Where: - `T_draw` = CTM * Translation(x,y) * Scale(destScale) - `T_origin` = element's computed transform-origin - `S_css_to_grid` = CSS pixels to canvas grid pixels ## Privacy-Preserving Painting `drawElementImage()` must not reveal security/privacy-sensitive information: **Excluded from painting:** - Cross-origin data in embedded content (iframes, images, url() refs, SVG use) - System colors, themes, preferences - Spelling/grammar markers - Visited link information - Pending autofill information - Subpixel text anti-aliasing **Allowed (not considered sensitive):** - Find-in-page / text-fragment markers - Scrollbar and form element appearance (already detectable via foreignObject) - Caret blink rate - forced-colors (already available via media query) ## Paint Event Timing (Option C — chosen approach) The paint event fires immediately after the browser's own Paint step, runs only once per frame. DOM invalidations during paint apply to the next frame, not the current one. This avoids the complexity and performance issues of looping approaches. --- # API Reference _Source: https://html-in-canvas.dev/docs/api-reference/_ # HTML-in-Canvas API Reference ## IDL Definitions ### HTMLCanvasElement Extensions ```idl partial interface HTMLCanvasElement { [CEReactions, Reflect] attribute boolean layoutSubtree; attribute EventHandler onpaint; void requestPaint(); ElementImage captureElementImage(Element element); DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform); }; ``` | Member | Type | Description | |--------|------|-------------| | `layoutSubtree` | `boolean` attribute | Opts canvas children into layout and hit testing. Reflected as HTML attribute `layoutsubtree`. | | `onpaint` | `EventHandler` | Handler for the `paint` event, fired when child rendering changes. | | `requestPaint()` | method | Forces a `paint` event to fire in the next frame, even if no children changed. Analogous to `requestAnimationFrame()`. | | `captureElementImage(element)` | method | Captures a snapshot of a child element as a transferable `ElementImage` for worker use. | | `getElementTransform(element, drawTransform)` | method | Returns the CSS transform to synchronize DOM position with a 3D draw transform. Used with WebGL/WebGPU. | ### OffscreenCanvas Extensions ```idl partial interface OffscreenCanvas { DOMMatrix getElementTransform((Element or ElementImage) element, DOMMatrix drawTransform); }; ``` ### CanvasDrawElementImage Mixin Applied to both `CanvasRenderingContext2D` and `OffscreenCanvasRenderingContext2D`. ```idl interface mixin CanvasDrawElementImage { // Draw at position DOMMatrix drawElementImage((Element or ElementImage) element, unrestricted double dx, unrestricted double dy); // Draw at position with destination size DOMMatrix drawElementImage((Element or ElementImage) element, unrestricted double dx, unrestricted double dy, unrestricted double dwidth, unrestricted double dheight); // Draw with source rect at position (no dest size) DOMMatrix drawElementImage((Element or ElementImage) element, unrestricted double sx, unrestricted double sy, unrestricted double swidth, unrestricted double sheight, unrestricted double dx, unrestricted double dy); // Draw with source rect at position with destination size DOMMatrix drawElementImage((Element or ElementImage) element, unrestricted double sx, unrestricted double sy, unrestricted double swidth, unrestricted double sheight, unrestricted double dx, unrestricted double dy, unrestricted double dwidth, unrestricted double dheight); }; ``` **Overload signatures** mirror `drawImage()`: | Signature | Description | |-----------|-------------| | `(element, dx, dy)` | Draw at (dx, dy), auto-sized to match on-screen proportions | | `(element, dx, dy, dw, dh)` | Draw at (dx, dy) scaled to (dw x dh) | | `(element, sx, sy, sw, sh, dx, dy)` | Draw sub-rect (sx, sy, sw, sh) at (dx, dy) | | `(element, sx, sy, sw, sh, dx, dy, dw, dh)` | Draw sub-rect scaled into dest rect | **Return value:** `DOMMatrix` — the CSS transform to apply to `element.style.transform` for synchronization. **Requirements:** - `layoutsubtree` must be set on the canvas - `element` must be a direct child of the canvas - `element` must have generated boxes (not `display: none`) - CSS transforms on the element are ignored for drawing - Canvas CTM is applied - Overflow clipped to element's border box **Snapshot behavior:** - During `paint` event: draws the current frame's snapshot - Outside `paint` event: draws the previous frame's snapshot - Throws if called before any snapshot has been recorded ### WebGL Extension ```idl partial interface WebGLRenderingContext { void texElementImage2D(GLenum target, GLint level, GLint internalformat, GLenum format, GLenum type, (Element or ElementImage) element); }; ``` Uploads the element's rendered content as a WebGL texture. Parameters match `texImage2D` but the source is an element instead of an image/canvas. **Usage pattern:** ```js gl.bindTexture(gl.TEXTURE_2D, texture); gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, element); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // LINEAR recommended for text ``` ### WebGPU Extension ```idl partial interface GPUQueue { void copyElementImageToTexture((Element or ElementImage) source, GPUImageCopyTextureTagged destination); }; ``` Copies element rendering to a WebGPU texture. ### PaintEvent ```idl [Exposed=Window] interface PaintEvent : Event { constructor(DOMString type, optional PaintEventInit eventInitDict); readonly attribute FrozenArray changedElements; }; dictionary PaintEventInit : EventInit { sequence changedElements = []; }; ``` | Member | Description | |--------|-------------| | `changedElements` | Frozen array of canvas children whose rendering changed since the last paint event. | ### ElementImage ```idl [Exposed=(Window,Worker), Transferable] interface ElementImage { readonly attribute unsigned long width; readonly attribute unsigned long height; undefined close(); }; ``` | Member | Description | |--------|-------------| | `width` | Width of the captured snapshot in pixels | | `height` | Height of the captured snapshot in pixels | | `close()` | Releases the underlying resources | `ElementImage` is `Transferable` — it can be sent to workers via `postMessage()`. ## Common Patterns ### Basic 2D Canvas ```html
Hello
``` ### OffscreenCanvas with Worker ```js // Main thread canvas.onpaint = () => { const img = canvas.captureElementImage(element); worker.postMessage({ elementImage: img }, [img]); }; worker.onmessage = ({ data }) => { element.style.transform = data.transform.toString(); }; // Worker self.onmessage = (e) => { if (e.data.elementImage) { ctx.reset(); const t = ctx.drawElementImage(e.data.elementImage, x, y); self.postMessage({ transform: t }); } }; ``` ### WebGL Texture ```js canvas.onpaint = () => { gl.bindTexture(gl.TEXTURE_2D, texture); gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, element); }; ``` ### WebGPU Texture ```js canvas.onpaint = () => { device.queue.copyElementImageToTexture(element, { texture: gpuTexture }); }; ``` ### Device Pixel Ratio Handling Always size the canvas grid to device pixels to prevent blurriness: ```js const observer = new ResizeObserver(([entry]) => { canvas.width = entry.devicePixelContentBoxSize[0].inlineSize; canvas.height = entry.devicePixelContentBoxSize[0].blockSize; }); observer.observe(canvas, { box: 'device-pixel-content-box' }); ``` --- # Design Decisions _Source: https://html-in-canvas.dev/docs/design-decisions/_ # Design Decisions & Rationale ## Why `layoutsubtree` as an attribute? The attribute serves as an explicit opt-in. Without it, canvas children are fallback content (for accessibility when canvas isn't supported). With it, children are promoted to first-class participants in layout and hit testing, but remain invisible until drawn. This dual role is key: the same elements serve as both the visual content (when drawn) and the accessibility tree. They're not separate — they're one and the same. ## Why CSS transforms on source elements are ignored When drawing an element, the canvas CTM controls positioning. If CSS transforms were also applied, you'd get double-positioning — the element's own CSS transform would compound with the canvas transform. Instead, CSS transforms are reserved for the synchronization step: after drawing, you set `element.style.transform` to the value returned by `drawElementImage()` so that the DOM position matches the drawn position. This separation keeps drawing and synchronization clean. **Important consequence:** Changing an element's CSS transform does NOT trigger a `paint` event, because transforms don't affect the element's painted output (only its position). ## Why `paint` fires after Paint (Option C) Three options were considered for when the `paint` event fires: **Option A — Resize observer timing (looping):** Would require synchronous Paint of canvas children, which is expensive and has implementation challenges in Gecko. Also, WebGL APIs like `getError()` would cause deadlocks when flushing. **Option B — After Paint with looping:** Even more expensive — more rendering steps run per loop iteration. **Option C — After Paint, no looping (chosen):** Runs once per frame. DOM changes during paint apply to the next frame. This mirrors the browser's own Paint step behavior. The key insight: by the time paint fires, the rendering update is locked in, except for the canvas content itself. ## Why `drawElementImage` returns a DOMMatrix Returning the synchronization transform directly from the draw call makes the common pattern trivial: ```js element.style.transform = ctx.drawElementImage(element, x, y).toString(); ``` The alternative would be a separate `getElementTransform()` call (which exists for WebGL/WebGPU where the transform isn't a simple 2D operation). ## Why direct children only? Restricting to direct children keeps the API simple and the containment model clear. Each drawn element has paint containment, is a containing block for its descendants, and has a stacking context. This means: - The element's rendering is self-contained - Overflow is predictable (clipped to border box) - Z-ordering within the element follows normal CSS rules - The canvas author controls the ordering of top-level elements ## Why `captureElementImage` instead of direct worker access? Workers can't access the DOM. Rather than creating a complex proxy mechanism, the design captures a snapshot as a transferable `ElementImage` object. This fits the existing `Transferable` pattern (like `ImageBitmap`) and keeps the worker API simple — workers just call `drawElementImage()` with an `ElementImage` instead of an `Element`. The trade-off: you need main-thread code to capture and transfer, and the transform needs to be communicated back to the main thread for synchronization. ## Why not `foreignObject`? SVG `foreignObject` already allows HTML in a graphics context, but: 1. It runs in the SVG rendering model, not the canvas model 2. No access to canvas 2D API transforms 3. No WebGL/WebGPU integration 4. Can't use canvas pixel manipulation 5. Performance characteristics differ 6. No `requestAnimationFrame`-style control HTML-in-Canvas is designed for the canvas use case specifically. ## Why not `html2canvas`? Libraries like `html2canvas` re-implement browser rendering in JavaScript. They're: 1. Incomplete — can't handle all CSS 2. Slow — re-parsing and re-rendering 3. Inaccurate — miss browser-specific rendering 4. Large — significant JS payload 5. Not interactive — produce static snapshots HTML-in-Canvas uses the browser's actual rendering engine, so it's complete, fast, accurate, small, and supports full interactivity. ## Privacy model The design follows a principle of not exposing information that isn't already available to JavaScript. Cross-origin content, system themes, visited links, spell-check indicators, and autofill previews are all excluded from painting. The key insight: since `drawElementImage` makes pixels readable via `getImageData()`, anything drawn must be "same-origin-equivalent" safe. This is the same security model as tainted canvases, applied proactively. Some new information IS exposed: - Form control rendering (already detectable via foreignObject) - Caret blink rate (low-entropy) - forced-colors mode (already queryable via media query) --- # Examples Analysis _Source: https://html-in-canvas.dev/docs/examples-analysis/_ # Official Examples Analysis All examples live at https://wicg.github.io/html-in-canvas/Examples/ ## 1. Complex Text (complex-text.html) **What it demonstrates:** Rich, rotated text with emoji, RTL, vertical text, inline images, and SVG — all rendered into canvas via a single `drawElementImage` call. **Key techniques:** - Canvas CTM rotation (`ctx.rotate`) — the drawn element follows the CTM - Multi-script text: LTR English, RTL Persian, vertical Chinese - Inline `` and `` inside the drawn element - DPR-aware translation (`80 * devicePixelRatio`) **Pattern:** ```js canvas.onpaint = (event) => { ctx.reset(); ctx.rotate((15 * Math.PI) / 180); ctx.translate(80 * devicePixelRatio, -20 * devicePixelRatio); let transform = ctx.drawElementImage(draw_element, 0, 0); draw_element.style.transform = transform.toString(); }; canvas.requestPaint(); // trigger initial paint ``` **Insight:** This is the simplest example — one element, one draw call, one transform sync. Shows how `drawElementImage` replaces what would otherwise require complex `ctx.fillText` with font metrics, bidi algorithm, and manual line breaking. --- ## 2. Pie Chart (pie-chart.html) **What it demonstrates:** A fully accessible, interactive pie chart with styled multi-line labels positioned radially. **Key techniques:** - Multiple children drawn in a loop - ARIA roles (`role="list"`, `role="listitem"`, `tabindex="0"`) - `ctx.drawFocusIfNeeded()` for focus ring rendering - Radial positioning using trig — labels centered at 60% of radius at each slice's midpoint angle - `data-*` attributes for chart data - Radial gradient fills per wedge **Pattern:** ```js for (const label of canvas.children) { // Draw wedge with Path2D const path = new Path2D(); path.arc(0, 0, radius, angle, angle + slice); ctx.fill(path); // Draw and position label const mid = angle + slice / 2; const x = Math.cos(mid) * radius * 0.60 - label_width / 2; const y = Math.sin(mid) * radius * 0.60 - label_height / 2; const transform = ctx.drawElementImage(label, x, y); label.style.transform = transform; } // Focus ring on top if (focusedPath) ctx.drawFocusIfNeeded(focusedPath, document.activeElement); ``` **Insight:** This is the accessibility showcase. The labels ARE the fallback content — screen readers see `role="listitem"` elements with the actual text. Tab navigation works, and `drawFocusIfNeeded` provides proper focus indication. This is the key value prop over `ctx.fillText()`. --- ## 3. Text Input / Interactive Form (text-input.html) **What it demonstrates:** A fully interactive HTML form (text inputs, checkboxes, radio buttons, range slider, button) rendered inside canvas. **Key techniques:** - Forms work normally — typing, clicking, tabbing all function - The `paint` event fires when form state changes (cursor blink, selection, input) - Simple positioning at `canvas.width/25, canvas.height/25` **Insight:** This proves that `layoutsubtree` preserves full interactivity. The form elements aren't simulated — they're real DOM elements with real event handlers, just drawn into the canvas. Cursor blinking triggers paint events automatically. --- ## 4. WebGL 3D Cube (webGL.html) **What it demonstrates:** HTML content rendered as a texture on a rotating 3D cube using WebGL. **Key techniques:** - `gl.texElementImage2D()` instead of `texImage2D()` - `gl.TEXTURE_MIN_FILTER = gl.LINEAR` — important for text quality (not mipmaps) - `gl.CLAMP_TO_EDGE` wrapping - `inert` attribute on the drawn element to prevent hit testing in this demo - Standard gl-matrix cube rendering - `requestAnimationFrame` loop for rotation **Pattern:** ```js function loadTexture(gl) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, draw_element); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); return texture; } canvas.onpaint = () => { main(); }; canvas.requestPaint(); ``` **Insight:** The `inert` attribute is noteworthy — it disables hit testing for the HTML element since it's mapped onto a 3D surface where 2D hit testing doesn't make sense. For interactive 3D HTML, you'd need to do raycasting yourself and forward events. --- ## 5. WebGPU Jelly Slider (webgpu-jelly-slider/) **What it demonstrates:** A range slider whose value is rendered as jelly-like 3D text on a ground plane, with physics simulation and ray marching — all using TypeGPU. **Key techniques:** - `copyElementImageToTexture()` for WebGPU - `canvas.requestPaint()` called on slider input - `
` as canvas children - Physics-based Verlet integration for jelly animation - SDF ray marching for 3D rendering - CSS custom properties for theming (`--jelly-color`, etc.) - Respects `prefers-reduced-motion` and `prefers-contrast` - TypeGPU framework for WGSL shader generation **Pattern (WebGPU):** ```js (canvas as any).onpaint = () => { (root.device.queue as any).copyElementImageToTexture( valueElement, width, height, { texture: valueRawTexture } ); // Manual transform sync (getElementTransform TODO noted in source) }; ``` **Insight:** Most complex example. Shows how HTML-in-Canvas enables mixing standard HTML controls (a range input) with advanced GPU rendering. The slider is a real `` — it's accessible, keyboard-navigable, and its value drives the 3D scene. The percentage text displayed on the ground plane is an HTML `
` captured as a GPU texture. --- ## Pattern Summary | Example | Context | API Used | Interactive | Accessible | |---------|---------|----------|-------------|------------| | Complex Text | 2D | `drawElementImage` | No | Yes (text content) | | Pie Chart | 2D | `drawElementImage` | Yes (focus/tab) | Yes (ARIA roles) | | Text Input | 2D | `drawElementImage` | Yes (full form) | Yes (form elements) | | WebGL Cube | WebGL | `texElementImage2D` | No (inert) | No | | Jelly Slider | WebGPU | `copyElementImageToTexture` | Yes (range input) | Yes | ## Common Boilerplate Every example follows this pattern: ```js // 1. Get context const ctx = canvas.getContext('2d'); // 2. Handle paint events canvas.onpaint = () => { ctx.reset(); // Clear and reset CTM // ... draw elements ... }; // 3. Request initial paint canvas.requestPaint(); // 4. Handle DPR new ResizeObserver(([entry]) => { canvas.width = entry.devicePixelContentBoxSize[0].inlineSize; canvas.height = entry.devicePixelContentBoxSize[0].blockSize; }).observe(canvas, { box: 'device-pixel-content-box' }); ``` --- # Open Questions _Source: https://html-in-canvas.dev/docs/open-questions/_ # Open Questions & Issues _Auto-synced from [`WICG/html-in-canvas` issues](https://github.com/WICG/html-in-canvas/issues) on 2026-08-31 via `scripts/sync-spec-docs.mjs`._ There are currently **0** open issues on the spec repository. Each heading below links to the upstream discussion — follow the link to read the full thread and leave a comment. _(No open issues — maybe the spec is perfect, or maybe the sync script hit a snag. Check the repo directly.)_ --- # Browser Support _Source: https://html-in-canvas.dev/docs/browser-support/_ # Browser Support _Auto-synced from [`WICG/html-in-canvas` README](https://github.com/WICG/html-in-canvas/blob/main/README.md) on 2026-08-31 via `scripts/sync-spec-docs.mjs`._ ## Status This is a living explainer which is continuously updated as we receive feedback. The APIs described here are implemented behind a flag in Chromium and can be enabled with `chrome://flags/#canvas-draw-element`. ## Developer Trial (dev trial) Information The HTML-in-Canvas features may be enabled with `chrome://flags/#canvas-draw-element` in Chrome Canary. We are most interested in feedback on the following topics: * What content works, and what fails? Which failure modes are most important to fix? * How does the feature interact with accessibility features? How can accessibility support be improved? Please file bugs or design issues [here](https://github.com/WICG/html-in-canvas/issues/new). ## How to try it You can run the demos on either Chrome Canary or a current Brave Stable — the flag lives in Chromium and rides along with any fork whose base milestone includes it. ### Option A — Chrome Canary 1. Install [Chrome Canary](https://www.google.com/chrome/canary/). 2. Visit `chrome://flags/#canvas-draw-element` and enable the flag. 3. Restart the browser. 4. Load any demo from the [demo gallery](/demos/). ### Option B — Brave Stable (Chromium 147+) Confirmed working on [Brave](https://brave.com/) Stable 1.89.132 / Chromium 147.0.7727.56. Older builds may not expose the flag. 1. Update Brave to a current Stable build (Menu → Brave → About Brave triggers an update). 2. Visit `brave://flags/#canvas-draw-element` and enable the flag. 3. Restart the browser. 4. Load any demo from the [demo gallery](/demos/). ## Other browsers - **Brave:** supported on recent Stable builds (≥ 1.89.132 / Chromium 147) behind `brave://flags/#canvas-draw-element`. - **Firefox:** no implementation — [details below](#firefox-support). - **Safari / WebKit:** no implementation announced. - **Edge / other Chromium forks:** the flag rides along wherever the underlying Chromium milestone has shipped the canvas-draw-element code. Try `chrome://flags/#canvas-draw-element` (or the fork's equivalent) on a recent build. ## Firefox support HTML-in-Canvas does not work in Firefox today. There is no implementation and no flag to enable — `drawElementImage()`, `layoutsubtree`, and the WebGL/WebGPU element-texture methods are unavailable, so the live demos on this site need a Chromium-based browser. Where Firefox actually stands: - Mozilla's formal review is open but undecided: [mozilla/standards-positions#1076](https://github.com/mozilla/standards-positions/issues/1076) (opened September 2024) is still marked "needs proposed position" — no position, positive or negative, has been published. - No Gecko implementation work has been announced. What works in Firefox today: the library approach — snapshotting DOM to canvas with html2canvas-style tools — runs in every browser, Firefox included. See [Render HTML to canvas: native API vs. html2canvas](/render-html-to-canvas/) for the comparison and code. This page is refreshed from the upstream spec repo, so Firefox status changes land here when they happen. ## Feedback Browser vendors and contributors track discussion at — see the [Open Questions](/docs/open-questions/) page for the current list. ---