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 null

In 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

  1. Security Limitations: Closed mode is not a security boundary. An external script can override Element.prototype.attachShadow before your component initializes to capture references to all created shadow roots.
  2. 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.
  3. 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.