Constructor and connectedCallback
The constructor: internal state only
Section titled “The constructor: internal state only”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 parsed —
this.getAttribute()returnsnull - 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 vs. right
Section titled “Wrong vs. right”// WRONG — DOM manipulation in constructorclass BadWidget extends HTMLElement { constructor() { super(); this.innerHTML = '<p>' + this.getAttribute('title') + '</p>'; // title is null here! }}
// RIGHT — DOM manipulation in connectedCallbackclass 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 }}Live demo
Section titled “Live demo”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.