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
| Callback | When 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.activeElementreturns 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.
ElementInternalsARIA 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.childreninconnectedCallback. Children may not have parsed yet. Listen forslotchangeinstead. - Forgetting to register attributes in
observedAttributes.attributeChangedCallbackis silently never called. - Mutating attributes in lifecycle callbacks without guard.
Setting an attribute that is in
observedAttributesre-triggersattributeChangedCallbackand 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,connectedCallbackwill re-set them up.
Further reading
- HTML Living Standard, §4.13 Custom elements.
- DOM Living Standard, §4.2.2 Shadow tree.
- Open UI’s research dossier on form-associated custom elements — the most thorough survey of constructor-side ergonomics.
- Declarative shadow DOM landed in 2023 in all three engines; Justin Fagnani’s explainer is the canonical reference.
- The 2024 Interop pass rates for the custom-elements suite are roughly 99% (Chromium), 95% (WebKit), 96% (Gecko); see wpt.fyi/custom-elements.