Home JS

Web Components: custom-element lifecycle and slots

A custom element's lifecycle is defined by four user-agent callbacks plus three observable transitions. Knowing them is the difference between a robust component and one that leaks.

What a custom element is

A custom element is a class extending HTMLElement (or a specialised element) registered with customElements.define(). Once registered, every occurrence of its tag in the document — including those parsed before registration — receives the class’s behaviour.

class WikiToc extends HTMLElement {
  static observedAttributes = ['active'];

  connectedCallback() {
    this.attachShadow({ mode: 'open' }).innerHTML = `
      <style>:host { display: block; }</style>
      <slot></slot>
    `;
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'active') this.#syncActive(newVal);
  }

  #syncActive(slug) {
    for (const a of this.querySelectorAll('a')) {
      a.setAttribute('aria-current', a.dataset.slug === slug ? 'page' : 'false');
    }
  }
}
customElements.define('wiki-toc', WikiToc);

The four lifecycle callbacks

CallbackWhen it fires
connectedCallback()Element is inserted into a document. May fire multiple times.
disconnectedCallback()Element is removed from a document.
attributeChangedCallback(name, oldValue, newValue)An attribute listed in static observedAttributes changes.
adoptedCallback(oldDoc, newDoc)Element moves between documents (rare).

connectedCallback may fire before the element’s children parse if the element is upgraded after parsing. Authors should not rely on child content being present in connectedCallback; instead, listen for slotchange on slots in the shadow root.

Shadow DOM in two minutes

this.attachShadow({ mode: 'open' }) creates a shadow tree attached to the element. The shadow tree:

  • Encapsulates styles. CSS inside the shadow root cannot leak out via the cascade; CSS outside cannot leak in (except for inherited properties and CSS custom properties).
  • Composes light DOM via <slot>. The light DOM (children authored by the consumer) is rendered into named or default slots in the shadow tree.
  • Has its own focus and selection scope. Tab order respects the flat tree; document.activeElement returns the host element when focus is inside.

The :host, :host-context(), and ::slotted() pseudo-classes let shadow-root CSS adapt to the host environment.

Form-associated custom elements

A custom element can participate in form submission with static formAssociated = true. The element gets an ElementInternals instance via this.attachInternals() and can call internals.setFormValue(...), internals.setValidity(...), and internals.ariaRole = 'textbox'. This is the right primitive for custom inputs that integrate with <form>, native validation, and the autofill pipeline.

Accessibility

Default ARIA semantics for a custom element are set via ElementInternals.role, ariaLabel, and the rest of the ARIAMixin properties. These set defaults that the element’s host can override via aria-* attributes — the so-called ARIA reflection model that reached interop in 2023 across Chromium (Blink), WebKit, and Gecko.

Browser engine support

  • Custom elements (autonomous) — interop since 2018 across Chromium (Blink), WebKit, and Gecko.
  • Shadow DOM attachShadow — interop since 2018.
  • Form-associated custom elements — interop 2023.
  • ElementInternals ARIA reflection — interop 2023.
  • Declarative shadow DOM (<template shadowrootmode>) — interop 2023.
  • Customised built-in elements (is="...") — supported in Chromium (Blink) and Gecko, not in WebKit. Avoid for cross-engine work.

Common pitfalls

  • Reading this.children in connectedCallback. Children may not have parsed yet. Listen for slotchange instead.
  • Forgetting to register attributes in observedAttributes. attributeChangedCallback is silently never called.
  • Mutating attributes in lifecycle callbacks without guard. Setting an attribute that is in observedAttributes re-triggers attributeChangedCallback and produces an infinite loop.
  • Style leakage through CSS variables. Custom properties cross shadow boundaries by design; they are not encapsulation breakers but are the recommended theming primitive.
  • Disposable resources in fields. Tear them down in disconnectedCallback — if the element is removed and re-added, connectedCallback will re-set them up.

Further reading