skip to content

In the custom elements API, what is the difference between an autonomous custom element and a customized built-in, and how do you declare and use each?

level: middleimportance: should knowfreq 35%

answer

  1. what the class extends decides everything
  2. third argument to define
  3. a standard tag wearing an is attribute
  4. one engine never shipped it
  5. inherit behaviour, lose shadow DOM

basics

~20 s

An autonomous custom element extends HTMLElement and is used as its own tag, such as <my-button>. A customized built-in extends a specific interface like HTMLButtonElement, is registered with { extends: 'button' }, and is used as <button is="my-button">, inheriting that element's behaviour.

solid answer

~50 s

An *autonomous* custom element extends `HTMLElement` directly, is registered with `customElements.define('my-button', MyButton)`, and is written as its own tag `<my-button>`. It starts from nothing: no built-in semantics, no keyboard behaviour, no form participation. A *customized built-in* extends a concrete interface — `class MyButton extends HTMLButtonElement` — is registered with a third argument `customElements.define('my-button', MyButton, { extends: 'button' })`, and is written as `<button is="my-button">` or created with `document.createElement('button', { is: 'my-button' })`. It inherits everything the built-in already does: the button role, keyboard activation, disabled handling, form submission. The catch is support: WebKit has declined to implement customized built-ins, so `is=` does nothing in Safari as of 2026 and Chromium and Firefox alone support it. That is why most shipped libraries use autonomous elements and re-implement the built-in behaviour, or wrap a real built-in inside one.

go deeper

for a junior

Be able to tell the two apart from markup alone: a hyphenated tag is autonomous, a standard tag with is="some-name" is a customized built-in, and only the second needs a third argument to define.

for a middle

Explain what each inherits — HTMLElement and nothing else, versus a concrete interface with its behaviour, role and form participation — and that the class must extend the interface matching the extends option.

for a senior

Weigh the real constraints before recommending one: Safari's non-implementation and the silent degradation it causes, the shadow-DOM restriction on most built-ins, and the wrap-a-real-button alternative with its own tradeoffs.

for a principal

Decide the house pattern for a component library and write down why: cross-engine reach usually rules out customized built-ins, which means owning the accessibility surface an inherited built-in would have given you for free, and budgeting for that.

## Two kinds of custom element The specification defines exactly two, and the difference is what the class extends. ### Autonomous ```js class FancyBox extends HTMLElement {} customElements.define('fancy-box', FancyBox); ``` ```html <fancy-box>content</fancy-box> ``` It gets `HTMLElement` and nothing more: no implicit ARIA role, no focusability, no keyboard behaviour, no form participation. Everything the component should do accessibly is your job — `tabindex`, key handling, and role/state exposure. ### Customized built-in ```js class PlasticButton extends HTMLButtonElement {} customElements.define('plastic-button', PlasticButton, { extends: 'button' }); ``` ```html <button is="plastic-button">Press</button> ``` The tag stays `button`; the `is` attribute names the definition to apply. Programmatic creation carries the same information: `document.createElement('button', { is: 'plastic-button' })`. Three rules follow from that shape: - The class must extend the interface that matches the tag named in `extends`. Extending `HTMLElement` while passing `{ extends: 'button' }` is a mismatch and fails. - The name still needs a hyphen, and it still must be unique in the registry. - The `is` attribute is only meaningful at element creation. Adding or changing it later on an existing element does nothing — you cannot convert a plain `<button>` into a customized one by setting `is` afterwards. ## What you gain and what you give up The attraction of a customized built-in is that you inherit a fully implemented element. `<button is="plastic-button">` is a real button: it is focusable, activates on Enter and Space, respects `disabled`, submits or resets its form, exposes the button role to assistive technology, and participates in form validation. Reproducing all of that on a `<div>`-shaped autonomous element is a lot of surface, and most hand-rolled attempts get some of it wrong. The cost is threefold: 1. **Support.** WebKit's implementers rejected customized built-ins on design grounds and have not shipped them, so as of 2026 `is=` is ignored in Safari: the element stays a plain `<button>` and your class never runs. A polyfill is available, and it is intrusive. This single fact is why customized built-ins are rare in shipped libraries. 2. **Shadow DOM.** `attachShadow` is permitted only on a fixed list of elements — `div`, `span`, `section`, `p`, `article`, `aside`, `header`, `footer`, `main`, `nav`, `blockquote`, `body` and the heading elements. `button` is not on it, so a customized `button` cannot have a shadow root and gets no style encapsulation. 3. **Discoverability.** `<button is="plastic-button">` reads as markup decoration rather than as a component, and tooling, templating layers and frameworks vary in how faithfully they pass the `is` attribute through. ## What teams actually do The common answer is an autonomous element that wraps a real built-in rather than extends one: `<plastic-button>` renders a genuine `<button>` inside its own tree and forwards clicks, focus and disabled state. You keep the built-in's behaviour where it matters and get an ordinary custom tag plus shadow-DOM encapsulation. The price is a wrapper layer — the outer element is not itself a button, so form association and label targeting have to be handled deliberately. For form controls specifically the platform has since grown a dedicated path for autonomous elements, so "I need a customized built-in to participate in forms" is no longer the argument it once was. ## Recognising each in the wild - A hyphenated tag on its own → autonomous. - A standard tag carrying `is="something-hyphenated"` → customized built-in. - `customElements.define` with a third argument → customized built-in; two arguments → autonomous. And one detail worth knowing for debugging: an unsupported `is` attribute degrades silently. There is no console error in Safari — the markup is valid HTML, the attribute is simply ignored, and the page renders an ordinary button with none of your behaviour. That silent degradation is what makes it a cross-browser hazard rather than a visible failure.

  • Can you turn an existing plain <button> into a customized built-in by setting its is attribute with JavaScript?
    No. The `is` value is consumed when the element is created — by the parser or by `document.createElement('button', { is: '...' })`. Setting the attribute later leaves the element an ordinary `HTMLButtonElement`; the attribute simply sits there. Create the element with the `is` option instead.
  • Why can a customized <button> not have a shadow root?
    Because `attachShadow` is allowed only on a fixed list of elements — mostly generic containers such as div, span, section, p and the headings — and `button` is not on it. So a customized built-in button inherits the built-in's behaviour but gets no style encapsulation, which pushes many teams toward an autonomous element wrapping a real button instead.
  • If customized built-ins are not universally supported, how do you get a button's behaviour in an autonomous element?
    Render a real `<button>` inside the component and forward the parts that matter — click, focus, disabled state — instead of re-implementing them on a generic element. You keep the built-in's keyboard handling and role for free; what you must handle deliberately is the outer element's relationship to labels and forms.

saying these in an interview costs you the question

  • Thinks setting is= on an existing element upgrades it
  • Assumes customized built-ins work in every browser
  • Passes { extends: 'button' } while extending HTMLElement
  • Believes an autonomous element inherits any built-in semantics
  • Expects a visible error when is= is unsupported

context