Skip to content

Open vs Closed Mode

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.

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.

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); // null

Note: you must capture the return value of attachShadow in a local variable — this.shadowRoot will be null for closed mode elements accessed from outside.

mode: 'open'mode: 'closed'
element.shadowRootReturns ShadowRootReturns null
DevTools inspectionYesLimited
Testing (querySelector on shadow)EasyHarder
Library support (Lit, etc.)FullRequires workarounds
SecurityNone beyond openMarginal (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.

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.

  • Choosing mode: 'closed' thinking it’s more secure — closed mode only hides the reference from .shadowRoot; the DOM can still be inspected through document.querySelector and 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 — since this.shadowRoot will be null, you must store the value attachShadow returns 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)

What does `element.shadowRoot` return when the shadow root was attached with `mode: "closed"`?
Which mode is recommended for components that need to work with browser DevTools and testing frameworks?
When using `mode: "closed"`, how do you keep a reference to the shadow root inside the component?