Skip to content

Add HTML-in-Canvas APIs#11588

Open
foolip wants to merge 64 commits intomainfrom
foolip/html-in-canvas
Open

Add HTML-in-Canvas APIs#11588
foolip wants to merge 64 commits intomainfrom
foolip/html-in-canvas

Conversation

@foolip
Copy link
Copy Markdown
Member

@foolip foolip commented Aug 21, 2025

  • At least two implementers are interested (and none opposed):
    • Chromium
  • Tests are written and can be reviewed and commented upon at:
  • Implementation bugs are filed:
    • Chromium: https://issues.chromium.org/u/1/hotlists/6783002
    • Gecko: …
    • WebKit: …
    • Deno (only for timers, structured clone, base64 utils, channel messaging, module resolution, web workers, and web storage): …
    • Node.js (only for timers, structured clone, base64 utils, channel messaging, and module resolution): …
  • Corresponding HTML AAM & ARIA in HTML issues & PRs:
  • MDN issue is filed: …
  • The top of this comment includes a clear commit message to use.

(See WHATWG Working Mode: Changes for more details.)


/canvas.html ( diff )
/dom.html ( diff )
/index.html ( diff )
/indices.html ( diff )
/infrastructure.html ( diff )
/references.html ( diff )
/rendering.html ( diff )
/webappapis.html ( diff )

Handwavy things that need fleshing out are marked with 👋
@foolip foolip marked this pull request as draft August 21, 2025 12:52
Copy link
Copy Markdown
Member

@Kaiido Kaiido left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see this being worked on, thanks.

Not quite sure how much discussion should be held at this stage. So to note, this doesn't seem to fully match the latest state of https://github.com/WICG/html-in-canvas. e.g. the rename to drawHTMLElement. The layoutsubtree attribute is also missing along with the implications to the existing fallback contents.
Still, thanks for making this move.

Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
@foolip foolip changed the title Stub out canvas.drawElement() Stub out canvas.drawHTMLElement() Aug 29, 2025
@foolip
Copy link
Copy Markdown
Member Author

foolip commented Aug 29, 2025

@Kaiido thank you for the review! I've fleshed things out more, renaming to drawHTMLElement() (because drawElement() will probably not be workable in WebGL) and adding the layoutsubtree attribute.

There's still some handwaving going on of course, in particular what causes the subtree to be laid out but not painted.

@foolip foolip mentioned this pull request Sep 2, 2025
Comment thread source Outdated
@foolip foolip changed the title Stub out canvas.drawHTMLElement() Stub out canvas.drawElementImage() Sep 16, 2025
@foolip foolip changed the title Stub out canvas.drawElementImage() Add HTML-in-Canvas APIs Sep 17, 2025
@foolip
Copy link
Copy Markdown
Member Author

foolip commented Sep 17, 2025

I've fleshed this out some more now, in particular the hit testing.

Copy link
Copy Markdown
Member

@Kaiido Kaiido left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One common complain with the use of dictionaries in the Canvas2D API is that this makes GC kick in very often during animations which has a non-negligible performance cost.
This API shape makes a big use of such dictionaries with one for the wrapper CanvasElementHitTestRegion and then a nested one for the CanvasHitTestRect, and I guess there will be scenarios where multiple of these will need to be updated at every frame. Since the values are copied over from the passed objects to new internal objects, it's unclear if even a careful author, who would try to reuse the same objects, could avoid GC at all here.

On the other hand, I really like how this API shape enables future additions like using a Path2D, or even a bitmap mask, instead of a CanvasHitTestRect. (btw can we bikeshed on rect for that purpose?)

It's not my area of expertise, but would an actual exposed interface allow for non copy from JS, so that authors can just update the regions instead of setting new ones?

Comment thread source Outdated
Comment thread source Outdated

<ol>
<li>
<p>For each <span>hit test region</span> <var>region</var> in <var>canvas</var>'s <span>hit test regions</span>:</p>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely an edge case, and I suppose it's a bit of a gray area (see whatwg/infra#396) but how is this supposed to work if setHitTestRegions is called during this iteration? E.g.

// Add multiple regions
ctx.setHitTestRegions([
  { element, rect: { x, y } },
  { element: anotherElement, rect: { x: anotherX, y: anotherY } }
]);
element.onclick = e => ctx.setHitTestRegions([]); // that was a 'once' handler

Should the anotherElement still perform the hit-test?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of problem is sometimes handled in the spec by making a frozen copy of the thing to iterate before starting iteration. Do you think that'd be OK here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would certainly be clearer as to what's supposed to happen yes. Now, whether it's the best behavior or not, I don't know and don't have any strong opinion. Both possibilities might come surprising depending on the case. The fact that the timing of hit-testing w.r.t. events propagation isn't well defined doesn't help...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@szager made a good point here which is that really when we start dispatching events hit testing is already done. So rather than making a copy of the list here, the spec here needs to make clear how the list is used in hit testing and that it all happens before event dispatch.

Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
@annevk annevk linked an issue Mar 26, 2026 that may be closed by this pull request
@tabatkins
Copy link
Copy Markdown
Contributor

Drive-by comment about styling/layout of the contents of a layoutsubtree canvas:

Canvas being a replaced element doesn't stop selectors from matching its children; that part already works normally.

It does prevent us from laying out the children, absent instructions on how to do so. I think what's expected here is that the elements lay out as normal children of the canvas, with the appropriate containments applied by the UA style sheet, right? Like, the canvas is just an ordinary block container as far as they're concerned. We can just specify that, and further say (already present in the spec) that they don't paint, but they do respond to hit-tests, selections, etc as normal. The element snapshot is then them doing a special paint operation.

Or does each child lay out independently, as if it's the sole child of the canvas, so that when you draw we can just snapshot from the canvas's 0,0 point and place that at the draw location? That works too, it just needs some clarifying text if so.

@AutoSponge
Copy link
Copy Markdown

Can we get a canvas method that renders CSS anchors at (X,Y) positions as shadowdom elements? Then, canvas children just anchor position to their respective points by name and we get to use actual HTML on canvas instead of images of HTML in canvas? Or is there another use case here that I don't see?

@progers
Copy link
Copy Markdown

progers commented Mar 28, 2026

Can we get a canvas method that renders CSS anchors at (X,Y) positions as shadowdom elements? Then, canvas children just anchor position to their respective points by name and we get to use actual HTML on canvas instead of images of HTML in canvas? Or is there another use case here that I don't see?

Hi @AutoSponge , are you asking why we don't use anchor positioning to move elements on top of the canvas, like https://github.com/shuding/cobe? This is a good question, as this API ultimately does need to keep the DOM synchronized with what is being drawn into the canvas. Our goal with this API is to enable patterns that are just not possible today, such as interleaving canvas drawing commands with html (e.g., jelly slider). The explainer lists the use cases this API is solving.

@AutoSponge
Copy link
Copy Markdown

In the explainer, what you're calling the <canvas> fallback others have used to build the semantic proxy layer (a hack that <canvas> never intended) that makes their UI accessible, and claiming there's some accessibility improvement/capability by this proposal. I've tried and I still don't see it. What's the % of scenarios where "fallback" content is perceived or interacted with visually?

I want to help devs use <canvas> but I don't want to see a proliferation of graphical UIs with no concern for accessibility. The explosion of TUIs and "GPU-powered" web interfaces are almost complete garbage for accessibility. If this is purely about putting CSS/browser-rendered content in <canvas>, then I'd recommend removing the accessibility claims. If I'm missing something, please help me understand. Thanks.

Comment thread source
data-x="dom-context-2d-drawElementImage">drawElementImage()</code>. An <span
data-x="concept-element-image-snapshot">element image snapshot</span> has a <dfn
data-x="concept-element-image-snapshot-width">width</dfn> and <dfn
data-x="concept-element-image-snapshot-height">height</dfn> (dimensions in <span>output
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably reference the "parent" canvas so that we can link to its output bitmap.

Comment thread source
<li><p>If <var>dw</var> and <var>dh</var> are given and either are infinite or NaN, then
return.</p></li>

<li><p>Let <var>snapshot</var> be the result of <span>get element image snapshot</span> given
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other Canvas2D API tend to silently not draw rather than throwing. But I don't know what to think about that legacy API design. Here in particular, since it also does return an object and isn't void like e.g. drawImage, it might be better to have it throw.
However I think other invalid attributes should also throw (maybe RangeErrors?) rather than silently return undefined (which will potentially cause throwing lower down the authors' code).

Comment thread source
<span>ImageBitmap</span> <span data-x="dom-OffscreenCanvas-transferToImageBitmap">transferToImageBitmap</span>();
<span data-x="idl-Promise">Promise</span>&lt;<span>Blob</span>> <span data-x="dom-OffscreenCanvas-convertToBlob">convertToBlob</span>(optional <span>ImageEncodeOptions</span> options = {});

<span>DOMMatrix</span> <span data-x="dom-OffscreenCanvas-getElementTransform">getElementTransform</span>((<span>Element</span> or <span>ElementImage</span>) element, <span>DOMMatrix</span> drawTransform);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Element should be removed here.
At first I was wondering if it would work with Web-IDL in workers, but that doesn't matter, the get element image snapshot algorithm would anyway not work with an Element since the context is bound to an OffscreenCanvas, not a <canvas>.

Comment thread source

<li><p><span>Assert</span>: <var>element</var> is an <code>Element</code>.</p></li>

<li><p>Let <var>canvas</var> be the <code>canvas</code> element to which <var>context</var> is
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be an assert canvas is a <canvas> and not an OffscreenCanvas?

Copy link
Copy Markdown
Member

@Kaiido Kaiido Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and context is actually undefined here, unlike draw an element above, this algorithm doesn't take a context input, and can be called from <canvas>'s or OffscreenCanvas's getElementTransform() methods directly, before there is any context.

Comment thread source
<var>element</var>.</p></li>

<li><p>Return the result of <span>get element transform</span> with <span>this</span>'s <span
data-x="offscreencanvas-placeholder">placeholder <code>canvas</code> element</span>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What when there is no placeholder?
And even when there is one, can this work synchronously across realms?

Comment thread source

<dd>
<p>Returns a <span>frozen array</span> of the elements that have changed, or the
<code>ElementImage</code> objects representing those elements for
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it still fire on OffscreenCanvas?

@hybridherbst
Copy link
Copy Markdown

Is it correct that so far only Chromium has shown intent to ship, or are there responses from Firefox/Webkit/… as well?

Comment thread source
<dl>
<dt>All content
<dd><span>CORS-cross-origin</span> data.
<dd>Non-default colors, fonts, themes, and preferences.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a non-default font? Or rather, what is a default font? If it must be a font that's available on all platforms, wouldn't that prevent basically all the fonts?
Moreover, aren't fonts already exposed by fillText() (minus a few settings that are already exposed through <foreignObject>)?

Also regarding this paragraph, are all these testable?

Comment thread source

<ul>
<li>
<p>Each child is laid out individually as if it were the only child.</p>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add:

For the purposes of this layout, the canvas parent is treated as having layout containment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

HTML-in-Canvas

9 participants