JavaScript attachShadow: Open vs Closed Shadow Roots
This article provides a concise guide on using the
Element.attachShadow() method in JavaScript to create
encapsulated Shadow DOM trees. You will learn the syntax required to
initialize a shadow root, understand the technical and behavioral
differences between the open and closed modes,
explore functional code examples, and discover best practices for
selecting the appropriate mode for your web components.
Understanding the attachShadow Method
The attachShadow() method attaches a shadow DOM tree to
a specified DOM element, which acts as the shadow host. This establishes
an isolated DOM subtree where internal markup and CSS styles do not leak
out to or collide with the main document.
The method accepts a single options object with a mandatory
mode property:
const shadowRoot = element.attachShadow({ mode: 'open' | 'closed' });The value assigned to mode determines whether the
internal structure of the shadow root is accessible from external
JavaScript via the host element.
Creating an Open Shadow Root
When you pass { mode: 'open' }, the browser permits
external JavaScript to access the shadow root through the host element’s
shadowRoot property.
const hostElement = document.createElement('div');
document.body.appendChild(hostElement);
// Attach an open shadow root
const shadow = hostElement.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p class="internal-text">Inside Open Shadow DOM</p>';
// Accessing from external script
console.log(hostElement.shadowRoot); // Returns the ShadowRoot object
console.log(hostElement.shadowRoot.querySelector('.internal-text').textContent); // "Inside Open Shadow DOM"In open mode: - The return value of attachShadow() is a
reference to the ShadowRoot. - External code can inspect,
modify, or query elements within the shadow root at any time using
hostElement.shadowRoot.
Creating a Closed Shadow Root
When you pass { mode: 'closed' }, the host element
denies external access to the shadow root by setting its
shadowRoot property to null.
const hostElement = document.createElement('div');
document.body.appendChild(hostElement);
// Attach a closed shadow root
const shadow = hostElement.attachShadow({ mode: 'closed' });
shadow.innerHTML = '<p class="internal-text">Inside Closed Shadow DOM</p>';
// Accessing from external script
console.log(hostElement.shadowRoot); // Returns nullIn closed mode: - The return value of attachShadow() is
the only direct reference to the ShadowRoot. - If you do
not save this reference within a private scope (such as a closure, a
WeakMap, or a private class field), external scripts cannot
interact directly with the internal shadow tree.
class CustomComponent extends HTMLElement {
#shadowRoot;
constructor() {
super();
// Save reference privately
this.#shadowRoot = this.attachShadow({ mode: 'closed' });
this.#shadowRoot.innerHTML = '<p>Encapsulated Content</p>';
}
internalMethod() {
return this.#shadowRoot.querySelector('p').textContent;
}
}
customElements.define('custom-component', CustomComponent);Key Considerations Between Modes
- Security Limitations: Closed mode is not a security
boundary. An external script can override
Element.prototype.attachShadowbefore your component initializes to capture references to all created shadow roots. - Testing and Tooling: Closed shadow roots make automated unit testing, debugging in developer tools, and certain accessibility implementations significantly harder because external utilities cannot traverse into the element.
- Best Practice: The web standard community
overwhelmingly recommends using
{ mode: 'open' }for custom elements. Closed mode is typically reserved for specialized browser-native implementations (like<video>or<audio>controls) or tightly controlled legacy components.