Attaching a Shadow Root
Calling attachShadow
Section titled “Calling attachShadow”To create a shadow tree, call this.attachShadow() on the host element and pass a mode option. The method returns the new shadow root and also stores it as this.shadowRoot.
class ScopedCard extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); }}You can call attachShadow in the constructor or in connectedCallback. The constructor is preferred when you want the shadow root available immediately; connectedCallback is fine when you also need access to attributes set in HTML.
Accessing the shadow root
Section titled “Accessing the shadow root”After calling attachShadow, the shadow root is available two ways:
// returned directly by the callvar root = this.attachShadow({ mode: 'open' });
// also stored as a property on the elementvar root = this.shadowRoot;Both references point to the same ShadowRoot object. The ShadowRoot behaves like a DocumentFragment — you can call querySelector, getElementById, and set innerHTML on it.
Rendering content with innerHTML
Section titled “Rendering content with innerHTML”The simplest way to populate the shadow tree is to set this.shadowRoot.innerHTML. Place your <style> tag first, followed by your markup:
this.shadowRoot.innerHTML = '<style>' + ' .card { border: 2px solid #6366f1; border-radius: 8px; padding: 1rem; background: #eef2ff; }' + ' h2 { color: #4f46e5; margin: 0 0 0.5rem; }' + '</style>' + '<div class="card"><h2>Shadow Card</h2><p>Styles scoped inside shadow root</p></div>';The styles only apply inside the shadow tree. Any .card element that exists in the light DOM is unaffected.
Full class example
Section titled “Full class example”class ScopedCard extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = '<style>' + ' .card { border: 2px solid #6366f1; border-radius: 8px; padding: 1rem; background: #eef2ff; }' + ' h2 { color: #4f46e5; margin: 0 0 0.5rem; }' + ' p { color: #3730a3; margin: 0; }' + '</style>' + '<div class="card"><h2>Shadow Card</h2><p>Styles scoped inside shadow root</p></div>'; }}customElements.define('scoped-card', ScopedCard);Runnable demo
Section titled “Runnable demo”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Call attachShadow in the constructor | The shadow root is ready immediately as soon as the instance is created | You still can’t read attributes from HTML at this point |
Call attachShadow in connectedCallback | You can read attributes declared in HTML from the very first time the element enters the DOM | You have to check whether a shadow root already exists in case the element is reconnected |
Common mistakes
Section titled “Common mistakes”- Calling
attachShadowmore than once — if the element is moved in the DOM (disconnectedCallbackfollowed byconnectedCallbackagain) and your code callsattachShadowagain every time, you get an error, because a shadow root can only be attached once per element. Checkthis.shadowRootbefore calling it again. - Setting
innerHTMLbefore callingattachShadow—this.shadowRootstaysnulluntilattachShadowhas been called; always call it first. - Concatenating HTML strings by hand from user input — building
innerHTMLlike the example above is convenient for a demo, but if the data comes from outside, usetextContentor sanitize it first, or you risk XSS.
💡 Real-world example
Salesforce Lightning (LWC) — calls
attachShadowas part of every component’s lifecycle, to encapsulate enterprise-scale UI built by many teams at once. (salesforce/lwc)