Custom elements expose a small set of lifecycle callbacks that the browser invokes at well-defined moments. This reference lists each one, when it fires, and what work belongs there.
How it works
You define callbacks as methods on a class extending HTMLElement:
class MyEl extends HTMLElement {
static get observedAttributes() { return ["value"]; }
constructor() { super(); this.attachShadow({ mode: "open" }); }
connectedCallback() { /* in the DOM — render, add listeners */ }
disconnectedCallback() { /* removed — clean up listeners/timers */ }
attributeChangedCallback(name, oldV, newV) { /* observed attr changed */ }
adoptedCallback() { /* moved to a new document */ }
}
customElements.define("my-el", MyEl);
The ordering rule that trips people up: the constructor may run before the element’s attributes and children exist (or before it is even in a document), so it must not touch them. Anything that reads the DOM, attributes, or parent belongs in connectedCallback.
Each callback in depth
constructor
The constructor is the first thing to run when an element is created, whether by the parser, document.createElement, or customElements.upgrade. The spec restricts what you can do here:
- You must call
super()first. - You must not read attributes, children, or the parent element. They may not exist yet.
- You must not dispatch events (nothing is listening yet).
- You may attach a shadow root (
this.attachShadow), set up instance variables, and callthis.attachInternals().
Trying to read this.getAttribute('value') in the constructor is one of the most common custom element bugs. The attribute will simply be absent — not an error — which leads to unexpected initial state.
connectedCallback
Fires every time the element is inserted into a document that has a browsing context. This is where rendering, event listeners, subscriptions, and attribute reading belong. It can fire multiple times if the element is moved in the DOM, so it must be idempotent — calling it twice should not double-attach listeners or double-render.
A pattern that handles the multiple-connect case:
connectedCallback() {
if (this._connected) return;
this._connected = true;
this._init();
}
disconnectedCallback() {
this._connected = false;
this._cleanup();
}
disconnectedCallback
Fires when the element is removed from a connected document. This is the cleanup hook: remove event listeners, cancel animation frames, clear timers, unsubscribe from observables. Failing to clean up here is the classic source of memory leaks in long-lived single-page applications, where components are added and removed frequently without a full page reload.
attributeChangedCallback(name, oldValue, newValue)
Fires when an observed attribute is added, changed, or removed. Only attributes listed in the static observedAttributes getter trigger this callback — unlisted attributes changes are silently ignored. The callback also fires once at upgrade time for each observed attribute that is already present on the element, allowing initial rendering from attribute values.
static get observedAttributes() { return ['color', 'size']; }
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'color') this._updateColor(newValue);
if (name === 'size') this._updateSize(newValue);
}
A null newValue means the attribute was removed; a null oldValue means it was just added.
adoptedCallback
Fires when the element is moved to a new document via document.adoptNode. This is rare in typical web applications but matters when transferring nodes between a main document and an iframe, or when using Document.importNode. Use it to re-bind any resources that are document-scoped (document-level event listeners, for example).
Timing relative to upgrade
Elements parsed from HTML are upgraded by the custom elements registry after parsing. During parsing the element exists as an HTMLElement without any custom logic. After customElements.define is called, all existing matching elements in the DOM are upgraded simultaneously — the constructor is called for each, followed by attributeChangedCallback for existing attributes, then connectedCallback if the element is in the document. This means the order of customElements.define relative to when the HTML is parsed matters for timing-sensitive initialization logic.
Tips and notes
connectedCallback/disconnectedCallbackcan fire multiple times as the node moves around the DOM. Make connect logic idempotent; always remove listeners and timers on disconnect.attributeChangedCallbackonly fires for attributes named in the staticobservedAttributesarray — and it fires once per such attribute already set at upgrade time.- Attach the shadow root in the constructor (it has no DOM dependency); render into it on connect.
- Use
adoptedCallbackonly when you move nodes between documents (rare). - Elements can be created and never connected — code that depends on
connectedCallbackhaving run must check readiness defensively if the element could be in a detached state.