Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Customized built-in elements via elementInternals.type #11061

Open
sanketj opened this issue Feb 20, 2025 · 5 comments
Open

Proposal: Customized built-in elements via elementInternals.type #11061

sanketj opened this issue Feb 20, 2025 · 5 comments
Labels
addition/proposal New features or enhancements

Comments

@sanketj
Copy link
Member

sanketj commented Feb 20, 2025

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: WebKit/standards-positions#97 (comment). 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: openui/open-ui#1088 (comment)
WHATWG resolution for elementInternals.type = 'button': openui/open-ui#1088 (comment)

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 sanketj added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest and removed needs implementer interest Moving the issue forward requires implementers to express interest labels Feb 20, 2025
@sanketj sanketj changed the title Proposal: elementInternals.type Proposal: Customized built-in element via elementInternals.type Feb 20, 2025
@sanketj sanketj changed the title Proposal: Customized built-in element via elementInternals.type Proposal: Customized built-in elements via elementInternals.type Feb 21, 2025
@rniwa
Copy link

rniwa commented Feb 21, 2025

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.

@keithamus
Copy link
Contributor

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);
  }
}

@sanketj
Copy link
Member Author

sanketj commented Feb 21, 2025

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.

@zzzzBov
Copy link

zzzzBov commented Feb 23, 2025

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
Copy link

zzzzBov commented Feb 23, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements
Development

No branches or pull requests

5 participants
@keithamus @rniwa @zzzzBov @sanketj and others