Open vs Closed Mode
The mode option
Section titled “The mode option”attachShadow requires a mode option. There are exactly two values: 'open' and 'closed'. The choice determines whether external JavaScript can access the shadow root through the element.shadowRoot property.
mode: ‘open’
Section titled “mode: ‘open’”With mode: 'open', the shadow root is exposed via element.shadowRoot:
class OpenBox extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = '<p>Open shadow root</p>'; }}customElements.define('open-box', OpenBox);
var el = document.querySelector('open-box');console.log(el.shadowRoot); // ShadowRoot { ... }External scripts, browser DevTools, and testing frameworks can all read and query the shadow root. This is the community standard and what libraries like Lit use.
mode: ‘closed’
Section titled “mode: ‘closed’”With mode: 'closed', element.shadowRoot returns null from outside the component:
class ClosedBox extends HTMLElement { connectedCallback() { var root = this.attachShadow({ mode: 'closed' }); // store root internally if you still need it root.innerHTML = '<p>Closed shadow root</p>'; }}customElements.define('closed-box', ClosedBox);
var el = document.querySelector('closed-box');console.log(el.shadowRoot); // nullNote: you must capture the return value of attachShadow in a local variable — this.shadowRoot will be null for closed mode elements accessed from outside.
Trade-offs
Section titled “Trade-offs”mode: 'open' | mode: 'closed' | |
|---|---|---|
element.shadowRoot | Returns ShadowRoot | Returns null |
| DevTools inspection | Yes | Limited |
| Testing (querySelector on shadow) | Easy | Harder |
| Library support (Lit, etc.) | Full | Requires workarounds |
| Security | None beyond open | Marginal (not a sandbox) |
Closed mode does not provide true security. Determined code can still work around it via monkey-patching Element.prototype.attachShadow. Use closed mode only when you have a specific API-hiding reason, not as a security measure.
Runnable demo
Section titled “Runnable demo”The demo defines two elements: <open-box> (mode: ‘open’) and <closed-box> (mode: ‘closed’). After they render, a small script reads element.shadowRoot on each and prints the result.
The <pre> output shows [object ShadowRoot] for the open element and null for the closed element.
Common mistakes
Section titled “Common mistakes”- Choosing
mode: 'closed'thinking it’s more secure — closed mode only hides the reference from.shadowRoot; the DOM can still be inspected throughdocument.querySelectorand some dev tools regardless. It is not a real security boundary — don’t use it as a substitute for actual data protection. - Using
mode: 'closed'and forgetting to keep your own reference to the shadow root — sincethis.shadowRootwill benull, you must store the valueattachShadowreturns in a variable or instance field yourself. - Choosing closed mode without considering tooling — test frameworks, DevTools inspectors, and many libraries (like Lit) expect
mode: 'open'. Choosing closed unnecessarily makes debugging and testing your component harder for no real benefit.
💡 Real-world example
Salesforce Lightning (LWC) — defaults components to
mode: 'open'so DevTools and automated tests can reach the shadow tree at enterprise scale. (salesforce/lwc)