html icon indicating copy to clipboard operation
html copied to clipboard

Proposal: Customized built-in elements via `elementInternals.type`

Open sanketj opened this issue 9 months ago • 11 comments

What problem are you trying to solve?

Web component authors often want to create custom elements that inherit the behaviors and properties of native HTML elements. These types of custom elements are referred to as "customized built-in elements" or just "customized built-ins". By customizing built-in elements, custom elements can leverage the built-in functionality of standard elements while extending their capabilities to meet specific needs. Some of the use cases enabled by customized built-ins are listed below.

  • Custom buttons can provide unique styles and additional functionality, such as split or toggle button semantics, while still maintaining native button behavior such as being a popover invoker.
  • Custom buttons can extend native submit button behavior so that the custom button can implicitly submit forms. Similarly, custom buttons that extend native reset button behavior can implicitly reset forms.
  • Custom labels can provide additional functionality, such as tooltips and icons, while still supporting associations with labelable elements via the for attribute or nesting a labelable element inside the custom label.

What solutions exist today?

A partial solution for this problem already exists today. Authors can specify the extends option when defining a custom element. Authors can then use the is attribute to give a built-in element a custom name, thereby turning it into a customized built-in element.

Both extends and is are supported in Firefox and Chromium-based browsers . However, this solution has limitations, such as not being able to attach shadow trees to (most) customized built-in elements. Citing these limitations, Safari doesn't plan to support customized built-ins in this way and have shared their objections here: https://github.com/WebKit/standards-positions/issues/97#issuecomment-1328880274. As such, extends and is are not on a path to full interoperability today.

How would you solve it?

The ElementInternals interface gives web developers a way to participate in HTML forms and integrate with the accessibility OM. This will be extended to support the creation of customized built-ins by adding a type property, which can be set to string values that represent native element types. The initial set of type values being proposed are listed below. Support for additional values may be added in the future.

  • '' (empty string) - this is the default value, indicating the custom element is not a customized built-in
  • button - for button like behavior
  • submit - for submit button like behavior
  • reset - for reset button like behavior
  • label - for label like behavior

This proposal was presented and discussed at TPAC, where it was resolved by the WHATWG to add elementInternals.type = 'button' to the HTML spec. Initial proposal discussed at TPAC: https://github.com/openui/open-ui/issues/1088#issuecomment-2366092981 WHATWG resolution for elementInternals.type = 'button': https://github.com/openui/open-ui/issues/1088#issuecomment-2372520455

This issue formally tracks the proposal against the @whatwg/html repo and also includes a few additional types beyond button that had support at TPAC.

Full explainer here: https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ElementInternalsType/explainer.md

Examples

Below is an example showcasing a custom button being used as a popup invoker. When the custom button is activated, ex. via a click, div id="my-popover will be shown as a popover.

    class CustomButton extends HTMLElement {
        static formAssociated = true;

        constructor() {
            super();
            this.internals_ = this.attachInternals();
            this.internals_.type = 'button';
        }
    }
    customElements.define('custom-button', CustomButton);
    <custom-button popovertarget="my-popover">Open popover</custom-button>
    <div id="my-popover" popover>This is popover content.</div>

Like with native buttons, if the disabled attribute is set, a custom button cannot be activated and thus cannot invoke popovers.


Below is an example showcasing a custom submit button being used to submit a form. When the custom button is activated, ex. via a click, the form will be submitted and the page will navigate.

    class CustomSubmitButton extends HTMLElement {
        static formAssociated = true;

        constructor() {
            super();
            this.internals_ = this.attachInternals();
            this.internals_.type = 'submit';
        }
    }
    customElements.define('custom-submit-button', CustomSubmitButton);
    <form action="http://www.bing.com">
        <custom-submit-button>Submit</custom-submit-button>
    </form>

If the disabled attribute is set on a custom submit button, it cannot be activated and thus cannot submit forms.


Below is an example showcasing a custom label being used to label a checkbox. When the custom label is activated, ex. via a click, the checkbox is also activated, resulting in its state changing to checked.

    class CustomLabel extends HTMLElement {
        static formAssociated = true;

        constructor() {
            super();
            this.internals_ = this.attachInternals();
            this.internals_.type = 'label';
        }
    }
    customElements.define('custom-label', CustomLabel);
   <custom-label for='my-checkbox'>Toggle checkbox</custom-label>
   <input type='checkbox' id='my-checkbox' />

cc: @alexkeng

sanketj avatar Feb 20 '25 23:02 sanketj

Not sure about the "type" property proposed here but enhancing ElementInternals to be able to implement more builtin-element-like behaviors seems like a good direction. Being able to implement a custom element which acts like a submit button, for example, seems like a natural enhancement to ElementInternals. Being able to implement a custom element which acts like a label also seems like a useful extension.

I think I'd prefer breaking apart features of these builtin elements and adding each capability to ElementInternals. That'll allow a custom element to combine a set of features even if a particular combination didn't exist as a builtin element.

rniwa avatar Feb 21 '25 08:02 rniwa

I think I'd prefer breaking apart features of these builtin elements and adding each capability to ElementInternals. That'll allow a custom element to combine a set of features even if a particular combination didn't exist as a builtin element.

If we're after more compositional "mixin" style features I wonder if we might consult with Lit folks (/cc @justinfagnani, @sorvell to name a few) about what an API shape might look like, especially with their work around the "Reactive Controllers" pattern. At the risk of scope creeping, providing a mixin pattern that allows for applying built-in behaviours, but also mixing in userland behaviours could be a really interesting concept.

import {CustomMixin} from './my-mixin.js'
class CustomButton extends HTMLElement {
  constructor() {
    super()
    const internals = this.attachInternals();
    // Add the built-in mixin for button activation behaviour, gives us `popovertarget`, `commandfor`, etc.
    internals.addMixin(HTMLButtonActivationBehaviorMixin);
    
    // Also adopt my custom mixin, which can do other things...?
    internals.addMixin(CustomMixin);
  }
}

keithamus avatar Feb 21 '25 08:02 keithamus

Not sure about the "type" property proposed here but enhancing ElementInternals to be able to implement more builtin-element-like behaviors seems like a good direction. Being able to implement a custom element which acts like a submit button, for example, seems like a natural enhancement to ElementInternals. Being able to implement a custom element which acts like a label also seems like a useful extension.

Glad to hear!

I think I'd prefer breaking apart features of these builtin elements and adding each capability to ElementInternals. That'll allow a custom element to combine a set of features even if a particular combination didn't exist as a builtin element.

If we break up the features and support them individually on ElementInternals (elementInternals.disabled, elementInternals.popoverTarget, elementInternals.for, elementInternals.control, etc.), developers will not have a way to get all the behaviors of a built-in at once. They will have to "pick and choose" the right ones and it could cause a bad user experience if they get that wrong. For example, if the author is creating a customized built-in button, it wouldn't get the button ARIA role implicitly and the author would need to remember to set elementInternals.role explicitly. With elementInternals.type, the custom button would get the implicit role in-built. An example of another quirk would be if a developer built a button that supports elementInternals.popoverTarget but didn't support elementInternals.disabled. Such a button could invoke a popover, like built-in buttons, but couldn't be disabled by the disabled attribute, unlike built-in buttons. That seems inconsistent, and unexpected given the goal is to provide built-in-like behavior.

Using elementInternals.type ensures that all these come in a "package", so the customized built-in always gets all the behaviors it needs to function like the corresponding native element.

sanketj avatar Feb 21 '25 22:02 sanketj

I think the proposal has merit, however I noticed that the API is somewhat unusual:

elementInternals.type should only be set once. If elementInternals.type has a non-empty string value and is attempted to be set again, a "NotSupportedError" DOMException should be thrown

To me, the requirement that the type only be assigned once tells me that it should not be a mutable property at all, but a parameter when constructing the ElementInternals instance. So instead of introducing a type property, we should be introducing an optional options parameter to attachInternals with a type property.

The value of the type should continue to be accessed as a property, but now it can be a read-only property with the same common behaviors as other read-only properties, rather than introducing surprising and unexpected behaviors.

zzzzBov avatar Feb 23 '25 04:02 zzzzBov

If we break up the features and support them individually on ElementInternals...developers will not have a way to get all the behaviors of a built-in at once.

Bitwise flags come to mind, where ElementInternals.BUTTON_TYPE could be the same as ElementInternals.SUBMIT_BEHAVIOR | ElementInternals.KEYBOARD_CLICK_BEHAVIOR | ElementInternals.SOME_OTHER_BEHAVIOR | .... Unfortunately that would be very difficult to set up in a way that supports backwards compatibility when we ultimately decide that there are other newer behaviors we'd like to support. Alternatively we could accept a string of space-separated tokens, or array of strings, or an object with properties.

I think the thing we'll want to identify is whether there are legitimate use-cases to opt in or out of some of the built-in behaviors. If there are, the API should enable those use cases.

That said, the API should make it easy to "fall into the pit of success" so that the common use cases (e.g. type = 'button') are trivially easy.

zzzzBov avatar Feb 23 '25 04:02 zzzzBov

To me, the requirement that the type only be assigned once tells me that it should not be a mutable property at all, but a parameter when constructing the ElementInternals instance. So instead of introducing a type property, we should be introducing an optional options parameter to attachInternals with a type property.

@zzzzBov this is great feedback! we agree that a mutable property is unnecessary, and an optional parameter to attacheInternals is a better direction. We'll update the explainer accordingly.

alexkeng avatar Feb 24 '25 19:02 alexkeng

I think the proposal has merit, however I noticed that the API is somewhat unusual:

elementInternals.type should only be set once. If elementInternals.type has a non-empty string value and is attempted to be set again, a "NotSupportedError" DOMException should be thrown

To me, the requirement that the type only be assigned once tells me that it should not be a mutable property at all, but a parameter when constructing the ElementInternals instance. So instead of introducing a type property, we should be introducing an optional options parameter to attachInternals with a type property.

The value of the type should continue to be accessed as a property, but now it can be a read-only property with the same common behaviors as other read-only properties, rather than introducing surprising and unexpected behaviors.

I like this idea, but I'm wondering if that makes it hard to customize the type based on the custom element's attributes? For example:

<my-button type="submit">Submit</my-button>
<my-button type="reset">Reset</my-button>

My understanding is that you should not / cannot access attributes in the constructor, so the attachInternals({type}) would require you to create separate custom elements for each button type?


Separately, could this proposal also support a custom element that acts as a Form element? e.g. type: 'form'

https://github.com/whatwg/html/issues/10220

frehner avatar Mar 18 '25 15:03 frehner

I'm wondering if that makes it hard to customize the type based on the custom element's attributes?

Either version of the proposal would make that approach impossible/inadvisable due to the fact that the type was meant be assigned exactly once, and attributes are mutable.

I don't have a strong argument against leaving it as a type property and allowing the value to be mutable.

I will say that <input> and <button>s use of type attributes to drastically change behavior for those elements is one of the most flagrant violations of the single responsibility principle, and your custom elements are probably better off by making use of different elements for different behaviors. In my own experience I can't think of a single time that I've ever changed a type attribute after an interactive element was initialized.

zzzzBov avatar Mar 18 '25 20:03 zzzzBov

Related: it should be possible to make an element behave like a form https://github.com/whatwg/html/issues/10220

This may require additional APIs, eg a way to submit the form, so it'd be good to come up with a pattern that doesn't design the platform into a corner.

jakearchibald avatar Mar 18 '25 21:03 jakearchibald

This is a great alternative to built-in extends (is="" attribute), but should be noted this doesn't cover the progressive enhancement use case. For example, if JavaScript is turned off (or takes a long time to load), then this,

<form>
  <cool-button></cool-button>
</form>

won't work, as the element will not do anything because JavaScript is required for ElementInternals. Same with <table>, etc.

Progressive enhancement is probably the most important thing that people need is="" for, because it cannot be solved with JavaScript (it needs to provide a suitable fallback in the absence of JavaScript). The rest, making custom buttons, etc, can already be done with JavaScript, despite if it is only more cumbersome but at least not impossible. Due to odd parsing rules, certain things like <table><foo-bar></foo-bar></table> simply won't work with or without JavaScript, due to how the parser sets up the DOM during parsing.

In this sense, the idea in this thread would be useful for making some things easier to do, but is not a complete alternative to customized built-in elements. This thread's idea would be a way to allow elements to gain the same behaviors as built-in elements using JavaScript, rather than extending them.

See the following issues for alternatives that would also cover progressive enhancement and odd parsing rules (all of which also do not extend built-ins but provide addon behaviors):

  • https://github.com/WICG/webcomponents/issues/727
  • https://github.com/WICG/webcomponents/issues/1029

Should those topics be moved here to whatwg?

trusktr avatar Mar 19 '25 02:03 trusktr

I'm not sure that using ElementInternals for this makes sense. So far that API doesn't really change basic element behavior. For example, formAssociated is not set via ElementInternals, but when an element is configured with it, functionality is exposed via ElementInternals.

The explainer notes that type can only be set once, but this means it can change (once) at any time in the element's lifecycle. This is likely unneeded and probably undesirable. Perhaps instead, "type" behavior should be configured similar to formAssociated via a static property:

class BehavesLikeButton extends HTMLElement {
  static formAssociated = true;
  static behavesLike = 'button';
  constructor() {
    super();
    this._internals = this.attachInternals();
    console.log(this.disabled);  // logs `false`
  }
}

sorvell avatar Jun 19 '25 00:06 sorvell

Perhaps instead, "type" behavior should be configured similar to formAssociated via a static property:

agreed this approach could provide a cleaner design and it also aligns with the proposal mentioned here for making an element behave like a form. (also agreed with @jakearchibald that form is a good candidate, but we'll focus on button and label to start)

I've filed https://github.com/whatwg/html/issues/11390 for further discussion.

alexkeng avatar Jun 20 '25 19:06 alexkeng

This is interesting what it enables, but odd in how it does: certain features will be runtime mixins of a form that cannot be implemented in userland, basically.

Can we use actual mixins instead?

class MyEl extends FormButton(HTMLElement) {}

Mixins enable type safety out of the box in TypeScript.

trusktr avatar Jun 24 '25 15:06 trusktr

I think this proposal is a bandaid solution that is overfit to the most prominent pain points, but in a way that does not architecturally fit the rest of the web platform. While it is tempting to just ship something, anything ASAP to fix the most pressing issues, any architectural weirdness we introduce is weirdness we will likely need to maintain forever, or at least for decades to come.

If this came to the TAG for a design review during my tenure, I'd raise these issues:

  • It involves an alternative "magic" inheritance mechanism that doesn't go through extends, which is the JS-native way to do class inheritance. super still resolves to HTMLElement not e.g. HTMLLabelElement or HTMLButtonElement, and any inherited properties/methods can not be customized via the usual JS mechanisms.
  • The values values are an odd mix of element types (button, label) and values (submit, reset).
  • The resulting element inherits some properties from the parent but not others, and there is no way for authors to conceptualize and predict which ones or pull in fewer or more.
  • The design violates several TAG principles around how properties are supposed to work, such as having IDL attributes with (huge) side effects or throwing on setters.

I just filed https://github.com/WICG/webcomponents/issues/1108 with an alternative that attempts to reuse existing web platform concepts such as class inheritance and slots. It's definitely more ambitious though: bigger scope, but also more powerful + much broader variety of use cases.

LeaVerou avatar Jul 20 '25 21:07 LeaVerou

I think this proposal is a bandaid solution that is overfit to the most prominent pain points, but in a way that does not architecturally fit the rest of the web platform. While it is tempting to just ship something, anything ASAP to fix the most pressing issues, any architectural weirdness we introduce is weirdness we will likely need to maintain forever, or at least for decades to come. If this came to the TAG for a design review during my tenure, I'd raise these issues:

@LeaVerou The solution is intentionally not using extends because inheritance is not always appropriate; a customized button like <fancy-button> doesn’t necessarily want everything from the native <button>. However, based on years of developer feedback, we know that <button> does have essential behaviors that authors repeatedly reimplement or ask browser vendors to expose when there is no viable workaround, so we introduced this opt-in mechanism that enables certain native capabilities, giving web component authors a better starting point so they don’t have to build everything from scratch. (also WebKit has long been opposed to the established inheritance-based approach to solving this class of problems (https://github.com/WebKit/standards-positions/issues/97), so using inheritance as the mechanism is unlikely to be interoperable)

button and label are just the starting point that's chosen based on what we understand to be the most pressing developer needs. The solution can be extended to other elements where there's user demand, so we don’t see it as overfitting

Regarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in https://github.com/whatwg/html/issues/11390 addresses both these concerns. The idea is that instead of using a mutable ElementInternals.type, we'd use a static behavesLike property that would support only (button, label).

alexkeng avatar Aug 01 '25 22:08 alexkeng

Thank you @LeaVerou for commenting on this, you managed to articulate what I failed to at (several) TPACs and in https://github.com/whatwg/html/issues/11061#issuecomment-2673966134. I'm hesitant about the proposed solution because it feels a little too narrow in scope and I worry we'll be limiting ourselves in future, and I also would love to find a solution which allows web component authors to "bring their own" behaviours (or polyfill emerging ones).

My concerns with both attachInternals().type = 'button' or static behavesAs = 'button':

  • Developers consistently provide feedback that string APIs are not desired. They can be brittle (for example what does static behavesAs = 'buton' do? Likely it can't throw).
  • They're not super discoverable, one cannot reason about them or the behaviours they will do in code alone, i.e. is no additional API surface to discover, and their behaviour cannot be reasonably explained with JavaScript. They're not explained by other primitives in the platform.
  • There is no defined path or carve-out for userland extension. For example if I want static behavesAs = 'frobulate' I can't, so I'm left to invent new ways or hack my way around to achieve the same effect.
  • There's no scope for composition. While label and button are likely mutually exclusive behaviours, we're locking ourselves into mutually exclusive behaviours. If we later decide to add a behaviour like image, we're limiting users from the ability to have both image+button behaviours.

It seems really unfortunate to me that we're exploring solutions for this shape of problem which, to me, is a missing feature of JS itself. If we were in C++ land we'd simply reach for multiple inheritance, or if in Rust we'd use a (dyn) trait, Swift we'd use a protocol, Kotlin we'd use an interface, in Ruby an include, the list goes on.

keithamus avatar Aug 02 '25 07:08 keithamus

@LeaVerou The solution is intentionally not using extends because inheritance is not always appropriate; a customized button like <fancy-button> doesn’t necessarily want everything from the native <button>.

Inheritance doesn't mean getting everything from the parent — that's what overrides are for.

However, based on years of developer feedback, we know that <button> does have essential behaviors that authors repeatedly reimplement or ask browser vendors to expose when there is no viable workaround, so we introduced this opt-in mechanism that enables certain native capabilities, giving web component authors a better starting point so they don’t have to build everything from scratch.

Oh I 100% agree that this is a problem that needs solving, I just have concerns about this proposed solution.

On that note, it would be useful for this discussion to make a more detailed/exhaustive list of these behaviors that authors repeatedly reimplement. It may reveal alternative solutions that we have not yet considered.

(also WebKit has long been opposed to the established inheritance-based approach to solving this class of problems (WebKit/standards-positions#97), so using inheritance as the mechanism is unlikely to be interoperable)

I’m well aware. But WebKit being opposed to that particular inheritance-based proposal (for good reasons) does not necessarily translate to them being opposed to any inheritance-based design.

button and label are just the starting point that's chosen based on what we understand to be the most pressing developer needs. The solution can be extended to other elements where there's user demand, so we don’t see it as overfitting

Regarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in #11390 addresses both these concerns. The idea is that instead of using a mutable ElementInternals.type, we'd use a static behavesLike property that would support only (button, label).

I may have missed it but I don't see how that addresses any of the concerns I listed except the one around mixing attributes and element names in the same value space? Additionally, because it is not inheritance based, it introduces a new problem: now components cannot have their own type attribute with different values and semantics if they want their component to behave like a button.


Edit: I wrote this before seeing @keithamus's comment above. +100 to literally everything in it.

LeaVerou avatar Aug 02 '25 23:08 LeaVerou

It seems really unfortunate to me that we're exploring solutions for this shape of problem which, to me, is a missing feature of JS itself. If we were in C++ land we'd simply reach for multiple inheritance, or if in Rust we'd use a (dyn) trait, Swift we'd use a protocol, Kotlin we'd use an interface, in Ruby an include, the list goes on.

JavaScript does have mixins, in the form of subclass factories: https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/

justinfagnani avatar Aug 03 '25 00:08 justinfagnani

On that note, it would be useful for this discussion to make a more detailed/exhaustive list of these behaviors that authors repeatedly reimplement. It may reveal alternative solutions that we have not yet considered.

I think this is a very important thing to do. This proposal does enumerate a list of attributes for each type, but it does not mention events, properties, or methods. Properties and methods are extremely important to think of how to handle, because in this proposal there is no reason to suspect that a static property on a class would suddenly induce new properties on it. That's certainly possible in a language as dynamic as JavaScript, but extremely rare for good reason. It would be very difficult to statically analyze, and exacerbate the fragile base class problem.

(also WebKit has long been opposed to the established inheritance-based approach to solving this class of problems (WebKit/standards-positions#97), so using inheritance as the mechanism is unlikely to be interoperable)

I’m well aware. But WebKit being opposed to that particular inheritance-based proposal (for good reasons) does not necessarily translate to them being opposed to any inheritance-based design.

My recollection of the WebKit position was that there were too many underspecified behaviors spread across the native elements to safely allow wholesale subclassing of them, and that they might be open to the idea of subclassing if someone could do the work and specify what should happen in all the cases (adding ShadowRoots, overriding connectedCallback, etc.)

This proposal starts with a narrow set of behaviors and seems to strive to specify them all, which is great and might just address WebKit's main complain with subclassing altogether. ie, instead of subclassing HTMLButtonElement a class would subclass something with the specific behaviors specified by the 'button' type in this proposal.

One thing that's important to not here is that this proposal really is a form of subclassing, just hidden from JavaScript. It's making HTMLElement itself polymorphic and able to express many different classes hidden within it.

I think this might become more clear if the proposal talked about attribute reflection and properties. Does type = 'button' mean that the attributes added also have the respective properties that they reflect to on HTMLButtonElement? Does the element get popoverTargetElement, etc.? How is this expressed in terms of the prototype chain? Are these all optional properties on HTMLElement?

IMO, this would be much more explainable if the mechanism for mixing in behavior was something that could be expressed in JavaScript, like a mixin. That would add new prototypes to the prototype chain, which have the new native-backed properties.

justinfagnani avatar Aug 03 '25 00:08 justinfagnani

a customized button like doesn’t necessarily want everything from the native

Inheritance doesn't mean getting everything from the parent — that's what overrides are for.

By everything I mean including things that overrides can't handle, eg the limitation of tweaking the shadow dom

Regarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in #11390 addresses both these concerns.

I may have missed it but I don't see how that addresses any of the concerns I listed except the one around mixing attributes and element names in the same value space?

My understanding is that the principles you referred to (6.2 and 6.4) apply to IDL attributes, and therefore do not appear to apply to the static property approach. In fact, 6.2 and 6.4 focus on the getters of IDL attributes, so even with the .type approach, I wouldn't see it as violating those principles, since the .type getter doesn’t throw or have side effects.

IMO, this would be much more explainable if the mechanism for mixing in behavior was something that could be expressed in JavaScript, like a mixin. That would add new prototypes to the prototype chain, which have the new native-backed properties.

Regarding the mixin approach, I agree it'll be more discoverable if we can see the added mixin in the prototype chain, however, there are some built-in behaviors that can't be captured by mixin, eg, the strict element hierarchy mentioned in this comment, or implicit label association, or the implicit submission mentioned in this comment. It appears if we were to pursue the mixin approach, we'd need a way to signal these implicit behaviors, eg adding a flag in the mixin, which essentially bring us back to our static property approach. Also, our current proposal supports only the relevant attributes and behaviors (eg, implicit label association, form submission etc); attribute reflection/properties/events/methods are out of scope and would still need to be implemented by authors, so there's really nothing new to expose in the prototype chain.

They're not super discoverable, one cannot reason about them or the behaviours they will do in code alone

If the mental model is, if behavesLike=button, all button attributes will be supported, would that be helpful? Or we can even explicitly enumerate what attributes we want (if not specified, default is all attributes):

static behaveLike = "button";
static supportedAttributes = ["popovertarget", "formnovalidate", "type"];

There is no defined path or carve-out for userland extension. For example if I want static behavesAs = 'frobulate' I can't, so I'm left to invent new ways or hack my way around to achieve the same effect

I am not sure I follow. If the goal is to reuse behaviors from a non-native element, couldn’t we just extend it? or do you mean you want to define a new collection of behaviors from native elements?

There's no scope for composition. While label and button are likely mutually exclusive behaviours, we're locking ourselves into mutually exclusive behaviours. If we later decide to add a behaviour like image, we're limiting users from the ability to have both image+button behaviours.

I am not sure if it's practical to create an element that combines capabilities from multiple built-in elements. These built-ins exist for a reason, and trying to blur their boundaries is likely to introduce unnecessary corner cases that could become unmanageable. I think we'd need to see more real-world use cases where we can't use <button><image></button> and <image+button> is the only viable option before we consider this direction.

alexkeng avatar Aug 08 '25 18:08 alexkeng

@alexkeng

a customized button like doesn’t necessarily want everything from the native .

Inheritance doesn't mean getting everything from the parent — that's what overrides are for.

By everything I mean including things that overrides can't handle, eg the limitation of tweaking the shadow dom

The inheritance-based proposal I linked to does cover that, through a mechanism of wrapping shadow roots (or discarding them entirely when they're not useful).

Regarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in #11390 addresses both these concerns.

I may have missed it but I don't see how that addresses any of the concerns I listed except the one around mixing attributes and element names in the same value space?

My understanding is that the principles you referred to (6.2 and 6.4) apply to IDL attributes, and therefore do not appear to apply to the static property approach. In fact, 6.2 and 6.4 focus on the getters of IDL attributes, so even with the .type approach, I wouldn't see it as violating those principles, since the .type getter doesn’t throw or have side effects.

Throwing is not the only possible side effect. The element class sprouting a bunch of IDL attributes that are defined either directly in the class or inherited is also a side effect.

LeaVerou avatar Aug 09 '25 17:08 LeaVerou

@alexkeng

our current proposal supports only the relevant attributes and behaviors (eg, implicit label association, form submission etc); attribute reflection/properties/events/methods are out of scope and would still need to be implemented by authors, so there's really nothing new to expose in the prototype chain.

I think that's a pretty critical hole in the current proposal then.

I think it would be pretty unexpected for a native-button-like element to have button-like attributes and not the button-like properties. It's especially bad to support the commandfor and popovertarget attributes, but not have commandForElement and popoverTargetElement properties, since those are a lot more than just returning the value of an attribute.

I'm also confused by the inclusion of labels for the button type, since this isn't an attribute, but a property (it's an IDL attribute) that reflects the NodeList of associated labels. What would it mean to support labels if properties can't be added to the prototype?

Were I in position to, I'd probably oppose this proposal based on this. At the very least there should be some way to get and set all attribute-associated properties, if not via get/setAttribute, then via ElementInternals like labels and form currently are. Though to me this is really a reason to go with a mixin-based approach in the first place.

justinfagnani avatar Aug 09 '25 21:08 justinfagnani

@justinfagnani I agree that supporting button-like properties would make the API surface more complete. One possible solution could be to introduce dedicated interfaces, eg. ButtonInternals / LabelInternals (names TBD) within ElementInternals:

interface ButtonInternals {
  attribute Element? popoverTargetElement;
  attribute DOMString popoverTargetAction;
  attribute Element? commandForElement;
};

interface LabelInternals {
  attribute DOMString htmlFor;
  attribute Element? control;
};

interface ElementInternals {
  attribute ButtonInternals? buttonMixin;
  attribute LabelInternals? labelMixin;
}

Example usage:

class CustomButton extends HTMLElement {
  static behavesLike = "button";
  
  constructor() {
    super();
    this.internals = this.attachInternals();
  }
  
  get popoverTargetElement() {
    return this.internals.buttonMixin?.popoverTargetElement ?? null;
  }
  
  set popoverTargetElement(element) {
    if (this.internals.buttonMixin) {
      this.internals.buttonMixin.popoverTargetElement = element;
    }
  }
}

This approach not only provides authors with a clear and discoverable API surface (rather than exposing these directly on ElementInternals), but also avoids potential property name collisions. It follows an existing pattern where some ElementInternals properties are enabled based on a static property, eg. .form/.labels are enabled by formAssociated = true, vs. .buttonMixin enabled by behavesLike = "button".

Our current proposal supports only attributes and implicit behaviors because we believe these alone could already provide meaningful value to authors and property support could be added incrementally later, but we are open to including properties in the first release if that makes more sense to the community. The main tradeoff is additional design/implementation time, eg. should the properties be added at the element level: fancy_button.popoverTargetElement? or the ElementInternals level: fancy_button.internals.popoverTargetElement? or the dedicated internals level: fancy_button.internals.buttonMixin.popoverTargetElement? (personally I think the last option makes the most sense.)

Finally, regarding the inclusion of the labels property in Button (or the control property in Label) in the explainer, that’s actually outdated info. Apologies for the confusion; we’ll update the explainer with the latest details soon.

alexkeng avatar Aug 13 '25 15:08 alexkeng

We discussed this topic a bit in this morning's WHATNOT. I was asked to give a bit of a historical perspective.

I think the overall goal of enabling custom elements to do everything built-in elements can do is a great one.

I think it is bad to do this via something like "make this element behave like tagName". That is too vague. Do you get the accessibility semantics? (Probably it should not, as now we have three layers, ARIA, ElementInternals, and your new thing.) Do you get the default CSS styles? Do you get the default activation behavior? Do you get their focus behavior? Do you get added to the appropriate element categories? (Labelable, submittable, etc. etc.) Nobody knows the answers with an API like that.

The API described here seems to on the surface resemble that antipattern. But I get the impression its authors are thinking deeply about these issues and their actual proposal is more subtle. I suggest they work hard to clarify exactly what algorithms/definitions in the spec they intend to modify, so that it can become clearer.


Historically, how we have done this is to decompose elements into various specific pieces of functionality, and expose them via ElementInternals. Examples include form-associated custom elements (FACEs) and accessibility semantics (ARIAMixin). (Arguably shadow DOM itself is another such example.)

This pattern means developers then have to expose the appropriate native element-like APIs via boilerplate wrapping. For example, lots of stuff like get form() { return this.#internals.form; }, or checkValidity() { return this.#internals.checkValidity(); }.

I have heard developers mention that this is unfortunate, and they'd prefer something more automatic. That is where a lot of the comments about mixins and inheritance seem to come in. I hear you. However, I think we should separate out the pattern we have, which works, for full-fidelity functionality, from any sugar layers that would make it easier to create this boilerplate. Personally, I think those sugar layers are a great fit for userland libraries, but if people want, we could explore something like CustomElementUtils.addFormAssociatedWrappers(MyElementConstructor), or something.

If we can hold our nose about the boilerplate, I think our current pattern is good. I think it gives you all the tools you need to create powerful new elements, with all the capabilities of native elements. I think we should keep doing it, with other things like https://github.com/whatwg/html/pull/5120.

Things get a little tricky when the behaviors we're exposing are not as general as https://github.com/whatwg/html/pull/5120, but instead are currently specific in the spec to 1-2 elements. That is where people can become tempted toward the "make this element behave like tagName" pattern. And then developers ask, can't you also give me all the boilerplate so I have the same API as tagName?

But if we recontextualize in the context of the historical project here, I think it's still very reasonable to say "submit buttons get special treatment in a few places in the spec. Custom elements should also be able to get that treatment." And then execute on our usual playbook, of adding something to ElementInternals.

Now, some specific API design considerations:

  • Historically, we have tried to make custom elements "inheritable", by putting some opt-ins as static properties (i.e. formAssociated = true) instead of on ElementInternals directly (i.e., a hypothetical this.#internals.formAssociated = true). That is, if you have MyFormAssociatedBaseClass, and then register class MyElement : MyFormAssociatedBaseClass {}, it should still end up form associated.

    I don't know how important this actually is. In particular, inheritance is less popular than we anticipated it being in 2016. But, absent a reason otherwise, I would probably continue defaulting to "static opt-in, which cases new parts of ElementInternals to start working", just like FACEs.

  • Whenever possible, we should allow new elements to remix and include all their favorite parts of existing elements. This is another thing that is not possible with "make this element behave like tagName".

    In this particular case, maybe it is the case that being a button, submit button, reset button, and label should all be mutually exclusive. But that argues for a less generic name than type (or behavesLike). Because what if I want something that behaves like both a submit button, and an image? Both a label, and an hr?

    I can't tell a better name before we perform the step mentioned above of analyzing exactly which parts of the spec you want to modify; are you mostly concerned about their effects on forms? On popovers? On focus? Etc.

  • Deciding how big or how small of a chunk to expose is a balancing act. Right now various things are tied up in being a button: e.g., interaction with forms, and interaction with popovers. Should we actually expose two APIs, so that an element can opt in to interacting with popovers without interacting with forms? Or only interacts with forms, and not popovers? Maybe not; maybe it's simpler for implementations to just keep those bundled together, and there's no known use case for separating them. But be careful.

domenic avatar Aug 22 '25 08:08 domenic

We discussed this topic a bit in this morning's WHATNOT. I was asked to give a bit of a historical perspective.

I think the overall goal of enabling custom elements to do everything built-in elements can do is a great one.

I think it is bad to do this via something like "make this element behave like tagName". That is too vague. Do you get the accessibility semantics? (Probably it should not, as now we have three layers, ARIA, ElementInternals, and your new thing.) Do you get the default CSS styles? Do you get the default activation behavior? Do you get their focus behavior? Do you get added to the appropriate element categories? (Labelable, submittable, etc. etc.) Nobody knows the answers with an API like that.

The API described here seems to on the surface resemble that antipattern. But I get the impression its authors are thinking deeply about these issues and their actual proposal is more subtle. I suggest they work hard to clarify exactly what algorithms/definitions in the spec they intend to modify, so that it can become clearer.

Thanks @domenic, I can definitely appreciate this. I think if I were to boil down the underlying intent, it's that while custom elements are the defacto "component model for the web", they aren't necessarily getting first-party support when we build out new features. Popover as an example was intended to be more generic and was constrained to native elements with semantics to ensure accessibility. That's fine and righteous but there's not a way to get support there without recreating the entire API in JS. We have FACE, but we remain plagued by 814. The ask for an anchor attribute will hopefully solve issues around anchored positioning support for custom elements. :has support for shadow dom seems like it may finally be coming, but support within :host was originally missing.

There's also the reality that when creating custom elements there is some special magic that the platform obfuscates for native elements that we just can't. Radios get associated via HTML automatically whereas custom element radios are "ARIA" radios and must be associated with a wrapping element which limits our ability in markup (no tables ¯_(ツ)_/¯ ). Similarly, a combobox or custom element select requires some kind of trigger which has to exist in the DOM, but this is obfuscated with native selects causing assistive tech behavior to feel different/odd. I'd love to use customizable select, but we're back in spec land because we can't slot into the native select element. These are all understandable but nevertheless unfortunate deltas where custom element authors are constrained.

I think the tension I feel is that custom elements are constantly championed as a use case for these new API's and then when we get to implementation support is suddenly lacking or missing completely.

What we're ultimately after can be summarized with this from this sentance:

Whenever possible, we should allow new elements to remix and include all their favorite parts of existing elements.

I ultimately don't have a preference for how we get there or the shape of the API, I'd just like to use these with custom elements because most all of them provide real, tangible value, that otherwise can only be recreated with loads of JavaScipt (if at all).

chrisdholt avatar Sep 03 '25 19:09 chrisdholt

This was briefly discussed in https://github.com/whatwg/html/issues/11622 today, see minutes. But we punted it to the next meeting when @dandclark can attend.

foolip avatar Sep 04 '25 09:09 foolip

I think @domenic's preferred approach of adding smaller new features to ElementInternals makes a lot of sense, because it's an obvious path to get started and allows for boilerplate reduction in the future.

Importantly, it takes the pressure off figuring out the shape of the boilerplate reducer just to satisfy the more pressing need of allowing custom elements to have submit, label, popover, or commandfor functionality. As long as a userland utility like a mixin can be written.

I personally think that mixins are the right API shape for bundling behaviors together, but the DOM hasn't vended a mixin yet that I'm aware of, so there might be a high bar to clear for adding the first.

Adding individual features to ElementInternals means we can propose this more general behavior bundling mechanism on its own, and the layering means that the platform has a clear path to vend these bundles in the future.

Inheritability

Inheritability is a double-edged sword.

I do think it's important to preserve inheritability at least somewhat (like FACE) in the API. Inheritance is used commonly with custom elements because they're already so often written as classes. Things like FormAssociated() mixins are a common way to wrap up the class static, ElementInternals, and public API parts that developers want out of boilerplate-reducers:

class MyInput extends FormAssociated(HTMLElement) { ... }

Mixins have a lot of flexibility to interact with imperative APIs like ElementInternals because they can override the constructor and such, but I see that mixins sometimes have to restore inheritability where it doesn't exist in the base API, but in a bespoke way. For instance ElementInternals#role:

class MyInput extends FormAssociated(HTMLElement) {
  static role = 'textbox';
}

The mixin then sets the role in the constructor.

Besides inheritability, most devs find the static property to be much simpler to use.

On the other hand, static properties limit the value to being the same across all instances of a class. We've already had cases where we want elements to be FACE or not on a per instance basis. Admittedly, this was for polyfilling scoped custom element registries where two classes registered to the same tag name might have different statics, but as an observation only the static properties like formAssociated or observedAttributes were a problem.

I'm unsure if there are non-polyfill use cases for setting these options on a per-instance basis.

It is interesting that a mixin can opt-in to exposing an ElementInternals API as a static class API. Maybe something like that is a way to have both.

justinfagnani avatar Sep 04 '25 16:09 justinfagnani

@alexkeng I'd love for you all to explore the direction @domenic outlines above. Small building blocks (e.g., maybe we can finally solve focus for custom elements) seems like the most likely way to success here.

annevk avatar Sep 12 '25 07:09 annevk

Discussed in https://github.com/whatwg/html/issues/11648#issuecomment-3293932780.

cwilso avatar Sep 15 '25 21:09 cwilso

Thank you all for the great discussion and feedback!

We have an updated explainer that includes these 4 major changes based on feedback here and WHATNOT discussions in last few weeks:

  1. Instead of exposing all button behaviors using static behavesLike = "button", we are focusing on exposing the activation behaviors only: static ButtonActivationBehaviors = true.
  2. We’ll expose only command/commandFor initially, with submit/reset as fast follows.
  3. ARIA behaviors (focusability, role=button) will be automatically included based on the ARIA WG’s overwhelming consensus.
  4. form association will NOT be automatically included. (TBD if we want to do it with submit/reset)

We believe this strikes a good balance, but we’d appreciate any additional thoughts.

alexkeng avatar Oct 10 '25 21:10 alexkeng