Skip to content

Constructor and connectedCallback

The constructor runs when the element is instantiated — either via new MyElement() or when the browser parses the HTML and creates a class instance. At this point:

  • The element may not yet be attached to any document
  • Attributes are not yet parsedthis.getAttribute() returns null
  • Children are not yet present — the element’s children haven’t been parsed yet
  • Setting innerHTML, textContent, or appending child nodes here will throw or produce unexpected results

The constructor’s job is narrow: call super(), set up internal JavaScript state (instance variables, default values, private Maps), and optionally attach a shadow root. Nothing more.

class MyWidget extends HTMLElement {
constructor() {
super(); // MUST be first — gives you access to `this`
// Good: set up internal state
this._count = 0;
this._listeners = [];
// Good: attach shadow root
this.attachShadow({ mode: 'open' });
// BAD — do NOT do this in constructor:
// this.innerHTML = '<p>hello</p>'; // may throw
// this.getAttribute('label'); // always null here
}
}

connectedCallback: rendering and listeners

Section titled “connectedCallback: rendering and listeners”

connectedCallback fires each time the element is inserted into a document (or shadow root). By this point:

  • The element is in the DOM
  • Attributes set in HTML markup have been parsed and are readable via getAttribute
  • It is safe to set innerHTML, manipulate children, and attach event listeners

connectedCallback can fire more than once. If you move an element from one parent to another using appendChild, it is first disconnected (triggering disconnectedCallback) and then reconnected (triggering connectedCallback again). Guard against double-initialization if needed.

class MyWidget extends HTMLElement {
constructor() {
super();
this._initialized = false;
}
connectedCallback() {
if (this._initialized) return; // guard against re-connection
this._initialized = true;
var label = this.getAttribute('label') || 'default';
this.innerHTML = '<span>' + label + '</span>';
this.addEventListener('click', this._handleClick.bind(this));
}
}
// WRONG — DOM manipulation in constructor
class BadWidget extends HTMLElement {
constructor() {
super();
this.innerHTML = '<p>' + this.getAttribute('title') + '</p>'; // title is null here!
}
}
// RIGHT — DOM manipulation in connectedCallback
class GoodWidget extends HTMLElement {
constructor() {
super();
// only internal state
this._title = '';
}
connectedCallback() {
this._title = this.getAttribute('title') || 'Untitled';
this.innerHTML = '<p>' + this._title + '</p>'; // attribute is available now
}
}

The element below appends a timestamped entry to the log for both constructor and connectedCallback. Use the buttons to remove and re-add the element and observe that connectedCallback fires each time while constructor fires only once.

Notice that constructor appears in the log only once — at element creation — while connectedCallback appears every time the element is re-inserted into the DOM.

Why should you not manipulate the DOM inside the constructor?
What does connectedCallback fire in response to?
When can connectedCallback fire more than once for the same element instance?