Web Components Lifecycle Callbacks

Custom element lifecycle callbacks with timing and connectedCallback vs constructor

Reference for custom element lifecycle callbacks — constructor, connectedCallback, disconnectedCallback, adoptedCallback and attributeChangedCallback — with when each fires and what work belongs in each phase. It runs free in your browser on Gera Tools, with nothing uploaded.

Last updated Source: Gera Tools

Why should I not access attributes in the constructor?

The spec forbids reading attributes, children or the parent in the constructor, because an element created via document.createElement is not yet upgraded and an element parsed from HTML may have its constructor run before its attributes and children are present. Do that work in connectedCallback instead, where the element is in the DOM.

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 call this.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 / disconnectedCallback can fire multiple times as the node moves around the DOM. Make connect logic idempotent; always remove listeners and timers on disconnect.
  • attributeChangedCallback only fires for attributes named in the static observedAttributes array — 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 adoptedCallback only when you move nodes between documents (rare).
  • Elements can be created and never connected — code that depends on connectedCallback having run must check readiness defensively if the element could be in a detached state.