Skip to content

Rendering Content

Why connectedCallback, not the constructor

Section titled “Why connectedCallback, not the constructor”

A custom element class has a constructor like any other ES class, but the constructor is the wrong place to do DOM work. When the browser calls the constructor, the element exists as a JavaScript object but it has not yet been added to the document — it has no parent, its attributes may not be fully parsed, and its children are not yet present.

Use connectedCallback instead. It fires each time the element is inserted into a connected document, at which point the element’s attributes and light DOM children are available:

class MyCard extends HTMLElement {
constructor() {
super(); // always required
// DO NOT touch this.innerHTML, this.children, or attributes here
}
connectedCallback() {
// Safe to read attributes and manipulate DOM here
const title = this.getAttribute('title') || 'Card';
this.innerHTML = '<h2>' + title + '</h2>';
}
}

innerHTML is the quickest way to stamp out a template:

connectedCallback() {
this.innerHTML = '<p>Hello from <strong>innerHTML</strong></p>';
}

For dynamic or user-supplied values, always sanitize first or use DOM APIs instead to avoid XSS:

connectedCallback() {
const p = document.createElement('p');
const userInput = this.getAttribute('label') || '';
p.textContent = userInput; // textContent never parses HTML
this.appendChild(p);
}

this.getAttribute(name) returns the attribute value as a string, or null if the attribute is absent. The pattern this.getAttribute('x') || 'default' is safe because null || 'default' evaluates to 'default':

connectedCallback() {
const color = this.getAttribute('color') || 'blue';
const label = this.getAttribute('label') || 'Button';
this.innerHTML =
'<button style="background:' + color + '">' + label + '</button>';
}

Try adding a second <color-card> tag with a different color and label in the HTML panel.

OptionBenefitCost
innerHTMLFast and concise to stamp out a templateRisks XSS if user-supplied values are inserted without sanitizing
DOM API (createElement, textContent)Safe from XSS because textContent never parses HTMLMore verbose and harder to read once the structure gets complex
  • Doing DOM work in the constructor instead of connectedCallback — the element isn’t inserted into the document yet, and attributes may not be fully parsed, so getAttribute can return something you didn’t expect.
  • Putting a user-supplied value directly into innerHTML — e.g. this.innerHTML = userInput opens the door to XSS. Use textContent or sanitize first.
  • Forgetting that getAttribute returns null, not an empty string — without a || 'default' fallback, the literal word "null" ends up concatenated into your output.

💡 Real-world examples

GitHub — <clipboard-copy> uses DOM APIs and textContent to read/write content instead of raw innerHTML, to avoid the security risk in production. (github/clipboard-copy-element)

Adobe Spectrum — components in Spectrum Web Components render their content in connectedCallback, the spec’s standard pattern, so attributes are always ready before the DOM is touched. (opensource.adobe.com)

Why should you avoid setting innerHTML in the constructor?
What does connectedCallback fire?
Which approach is safer when rendering user-supplied data?
What does getAttribute return when an attribute is not present on the element?