Which lifecycle callbacks does the browser call on a custom element registered with customElements.define(), and what triggers each one?
answer
- four callbacks, no more
- one of them is gated by a static getter
- insertion, removal, adoption, attribute
- read once at define time
- fires again on every move
basics
~20 sFour: connectedCallback when the element becomes connected to a document, disconnectedCallback when it is removed, adoptedCallback(oldDoc, newDoc) when it moves to another document, and attributeChangedCallback(name, oldValue, newValue) for attributes listed in the static observedAttributes array.
solid answer
~40 sA custom element class can implement four reaction callbacks. `connectedCallback()` runs every time the element becomes connected to a document — insertion, and again after every move. `disconnectedCallback()` runs when it is removed. `adoptedCallback(oldDocument, newDocument)` runs when the element is adopted into a different document, for example moved into an iframe. `attributeChangedCallback(name, oldValue, newValue)` runs when an attribute changes, but only for names returned by the `static get observedAttributes()` getter — anything not listed is invisible to it, and that getter is read once at definition time. The callbacks are custom element reactions, invoked synchronously as part of the DOM operation, so `parent.appendChild(el)` has already run `connectedCallback` by the time it returns. Attribute callbacks for attributes present in the markup fire before the first `connectedCallback`, so the element sees its configuration before it renders.
go deeper
Be able to name all four callbacks and say what triggers each, and remember that attributeChangedCallback needs the static observedAttributes list to fire at all.
Explain the ordering — observed attributes are delivered before the first connectedCallback — that observedAttributes is read once at define time, and that the callbacks run synchronously inside the DOM operation rather than as later events.
Show the production consequences: repeated connectedCallback on moves, a slow callback stalling appendChild, no batching across successive attribute writes, and disconnectedCallback not firing on page unload.
Argue where the line sits between hand-written custom elements and a base library: the platform gives four raw hooks and no reactivity or batching, so decide whether your teams re-implement that per component or standardise on one base class.
## The four callbacks The custom elements spec defines exactly four lifecycle callbacks (formally: *custom element reactions*). You implement them as ordinary methods on the class; the browser calls them if they exist and ignores them if they do not. ```js class MyToggle extends HTMLElement { static get observedAttributes() { return ['checked', 'label']; } connectedCallback() {} disconnectedCallback() {} adoptedCallback(oldDocument, newDocument) {} attributeChangedCallback(name, oldValue, newValue) {} } customElements.define('my-toggle', MyToggle); ``` ### connectedCallback Runs whenever the element becomes *connected* — that is, whenever its root is a document. Creating it with `document.createElement('my-toggle')` does not fire it, and neither does appending it to a detached `<div>`; only landing in the document tree does. It is where rendering, subscriptions and DOM reads belong. The crucial detail is *whenever*, not *the first time*. Moving an element with `otherParent.appendChild(el)` removes it and re-inserts it, so `disconnectedCallback` runs and then `connectedCallback` runs again on the same instance. ### disconnectedCallback Runs when the element is removed from the document. It is the teardown hook — unsubscribe, abort in-flight work, clear timers. It is *not* called when the page unloads or the tab closes, so it is not a place to flush anything you care about. ### adoptedCallback Runs when the element is adopted into a different document — for example `iframe.contentDocument.body.appendChild(document.adoptNode(el))`. Its two arguments are the old and new documents. Most components never implement it; it matters when you cache references to `document` or `window` that are now the wrong ones. ### attributeChangedCallback Runs for attribute additions, changes and removals, with the attribute's local name, the previous value and the new value. On removal, `newValue` is `null`; on addition, `oldValue` is `null`. Setting an attribute to its current value still fires it. It fires **only** for attributes named in `static get observedAttributes()`. That getter is read exactly once, when `customElements.define()` runs, and the result is frozen — mutating the array afterwards changes nothing. Forgetting to list an attribute is the single most common reason people report that "attributeChangedCallback never fires". ## Ordering, and why it matters For an element the HTML parser creates with attributes already on it, the attribute callbacks are queued for each observed attribute *before* `connectedCallback`. The same holds for an upgrade: when a definition arrives for elements already in the document, the browser replays `attributeChangedCallback` for every observed attribute currently present (with `oldValue` of `null`) and then, if the element is connected, calls `connectedCallback`. So the reliable pattern is: use `attributeChangedCallback` to record state on the instance, and do the actual rendering in `connectedCallback`, which is guaranteed to run after the initial attributes have been seen. ```js attributeChangedCallback(name, oldValue, newValue) { if (name === 'label') this._label = newValue; if (this.isConnected) this._render(); } ``` The `this.isConnected` guard is what keeps the pre-connection replay from rendering into a not-yet-live element. ## They are synchronous reactions Callbacks are not events and they are not queued for later. They are invoked as reactions to the DOM operation that caused them, so: ```js container.appendChild(el); // connectedCallback has already run here ``` That means a slow `connectedCallback` makes `appendChild` slow, and an exception thrown inside one surfaces as an unhandled error rather than rejecting anything the caller can catch. Keep them cheap. ## What is not in the list There is no "rendered", "updated" or "before remove" callback, and no callback for property assignment — only these four. Everything frameworks give you on top (reactive properties, render scheduling, batched updates) is library code built over `attributeChangedCallback` and setters. Plain custom elements have no batching: five attribute writes in a row mean five callback invocations, which is why hand-written components usually coalesce rendering themselves.
- Does connectedCallback fire when you create the element with document.createElement and append it to a detached div?No. The callback fires on becoming *connected*, meaning the element's root is a document. `createElement` alone constructs the instance and nothing else; appending into a detached subtree keeps it disconnected. Only when that subtree is inserted into the document does `connectedCallback` run — at that point for every custom element in it.
- Why might attributeChangedCallback never fire even though the attribute clearly changes?Almost always because the attribute is not in `static get observedAttributes()`. That getter is read once at `customElements.define()` time and the list is fixed afterwards, so a typo, a late mutation of the array, or listing a camelCase name instead of the lowercase attribute name all produce silence rather than an error.
- An element is inserted, then immediately moved to another container. What sequence of callbacks does that produce?connectedCallback, then disconnectedCallback, then connectedCallback again on the same instance — a move is implemented as a removal plus an insertion. Setup done in connectedCallback therefore has to be idempotent, and anything it registers has to be released in disconnectedCallback or it accumulates on every move.
saying these in an interview costs you the question
- Thinks connectedCallback fires only once per element
- Expects attributeChangedCallback without declaring observedAttributes
- Says document.createElement triggers connectedCallback
- Treats the callbacks as async events fired after the DOM operation
- Expects a callback for property assignment or for re-render