Spike: HTML-in-Canvas
Issue #8. Question: can an effect distort an element's own pixels (the text itself bleeding like ink) instead of drawing a cover over it, without losing what makes the element native? Live page: playground/spikes/html-in-canvas.html.
The API, as of November 2026
Chromium 147–156, behind the CanvasDrawElement flag |
Chrome 157+ | |
|---|---|---|
| Opt-in on the canvas | <canvas layoutsubtree> |
<canvas content="drawable">, [drawable] on each child drawn |
| WebGL upload | gl.texElementImage2D(target, internalformat, element) |
gl.texElementSubImage2D(...) |
| Repaint signal | paint event, canvas.requestPaint() |
same |
| Firefox, Safari | none | none |
The spike implements the first shape only (verified on Chromium 153 with the flag). In a browser with the shipped shape it keeps the article as plain DOM.
Chrome 158 (CI, November 2026) showed why the shipped shape is not a rename. With the article moved into a drawable canvas, clicks went to the canvas instead of the children: the shipped API expects the page to declare where each element is drawn (element geometry: updateElementGeometry, getElementTransform, the elementgeometryupdate event) so that hit testing can map pointer events to it, and it uploads with texElementSubImage2D. Porting the spike means drawing the element where the geometry says and keeping that geometry up to date; that is the next step before any API proposal.
What was verified
Chromium 153 + flag, headless, SwiftShader; each item is an e2e test (tests/e2e/spike-html-in-canvas.spec.ts).
| Check | Result |
|---|---|
| Element uploaded as a WebGL texture | yes: the article's text comes back in the canvas pixels |
| Effect on the element's own pixels | yes: at half progress the letters themselves are displaced and darkened along the front |
| Accessibility tree | the children keep their roles and names (link, button, text) |
| Hit testing | clicking the button inside the canvas fires its handler |
| Keyboard | Tab moves into the link inside the canvas; the focus ring is painted (through the paint event) |
| Find-in-page | window.find() finds text inside the canvas |
| Selection | triple click selects the paragraph inside the canvas |
| Without the API | the article stays plain DOM; no canvas is created |
Not verified: the Chrome 157+ shape (see above), context loss (the spike reloads), high-DPI precision, performance, WICG issue #172 (it concerns a WebGL canvas moved into a drawable canvas; here the drawable canvas is the WebGL one).
What it means for an API
- The canvas must be created at runtime, and only where the API exists. Canvas children are fallback content: a browser that has
<canvas>but not this API never paints them. Authors cannot write the canvas in their markup. - The canvas becomes the element's only display. It must draw on every
paintevent, forever, not only while animating. That breaks the current model (one shared canvas, attached only during an animation): each drawable element needs its own canvas and WebGL context, and browsers cap contexts (around sixteen). It can only be an opt-in for a few elements per page. - The element moves. It becomes a child of the canvas: selectors such as
main > article, layout (the canvas must be sized to its content, here with aResizeObserver) and margins change. That is visible to the page's CSS and scripts. - The native contract seems to hold for what was tested: semantics, focus, hit testing, search and selection all stay with the real element.
Verdict
Promising and worth keeping as a progressive layer, not ready for the core. Proposed shape for a later issue, once Chrome 157 is verified and the API stops moving:
- a separate entry point, e.g.
createDrawableFx(element, { effect }), Chrome-only, feature-detected; - the effect receives the element as
uElementand must draw it exactly at rest (the canvas is the display); - everywhere else, the existing cover effects (details, swap, image) are the fallback, unchanged.
Re-evaluate when Firefox or WebKit signal an implementation.