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.