How composedPath Traces Events in Shadow DOM

The Event.composedPath() method in JavaScript returns an array of DOM nodes representing the exact hierarchy an event travels through during propagation, from the originating target up to the Window object. When dealing with Web Components, it serves as the primary mechanism to inspect event propagation across encapsulated Shadow DOM boundaries. This article explains how composedPath() handles event retargeting, distinguishes between open and closed shadow roots, and builds the complete propagation path across shadow boundaries.

Understanding Event Retargeting in Shadow DOM

By default, the Shadow DOM provides encapsulation so that internal implementation details of a Web Component remain hidden from the outer document. When an event fires on an element inside a shadow tree and bubbles out into the main DOM, the browser performs event retargeting.

During retargeting, the event.target property is dynamically rewritten. To any event listener outside the custom element, event.target points to the host custom element rather than the internal shadow node where the interaction actually occurred.

// Listener on the document
document.addEventListener('click', (event) => {
  console.log(event.target); // Output: <my-button> (the host element)
});

While event.target maintains encapsulation by masking internal elements, developers often need access to the exact origin of the event. This is where event.composedPath() is required.

How composedPath() Traverses Boundaries

When invoked, composedPath() bypasses standard retargeting to construct an ordered array of every node the event traverses across both the light DOM and shadow trees.

The path is constructed according to specific rules:

  1. Origin Identification: The path begins at the deepest node where the event was dispatched (the original target).
  2. Shadow Tree Traversal: The event ascends through the internal nodes of the shadow tree up to the ShadowRoot.
  3. Boundary Crossing: If the event has its composed property set to true, the event crosses the shadow boundary and continues to the host element.
  4. Light DOM Traversal: The path continues upward from the host element through its ancestor elements in the light DOM, reaching document and finally window.
// Given: <my-element> -> #shadow-root -> <button>Click me</button>
button.addEventListener('click', (event) => {
  console.log(event.composedPath());
  // Output: [button, shadowRoot, my-element, body, html, document, Window]
});

The Role of the composed Flag

Not all events can cross shadow boundaries. An event’s ability to be traced into the outer DOM depends on its composed initialization property:

Open vs. Closed Shadow Roots

The visibility of nodes within composedPath() is strictly determined by the mode of the shadow root:

Summary of Path Construction

The composedPath() algorithm creates a linear snapshot of propagation by: * Recording the dispatch target and each parent element in order. * Crossing shadow boundaries if composed: true. * Filtering out closed shadow nodes when evaluated from external scopes. * Terminating at the root Window object.