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>'; }}Setting content: innerHTML vs DOM APIs
Section titled “Setting content: innerHTML vs DOM APIs”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);}Reading attributes
Section titled “Reading attributes”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>';}Runnable demo
Section titled “Runnable demo”Try adding a second <color-card> tag with a different color and label in the HTML panel.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
innerHTML | Fast and concise to stamp out a template | Risks XSS if user-supplied values are inserted without sanitizing |
DOM API (createElement, textContent) | Safe from XSS because textContent never parses HTML | More verbose and harder to read once the structure gets complex |
Common mistakes
Section titled “Common mistakes”- 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, sogetAttributecan return something you didn’t expect. - Putting a user-supplied value directly into
innerHTML— e.g.this.innerHTML = userInputopens the door to XSS. UsetextContentor sanitize first. - Forgetting that
getAttributereturnsnull, 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 andtextContentto read/write content instead of rawinnerHTML, 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)