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:
- Origin Identification: The path begins at the deepest node where the event was dispatched (the original target).
- Shadow Tree Traversal: The event ascends through
the internal nodes of the shadow tree up to the
ShadowRoot. - Boundary Crossing: If the event has its
composedproperty set totrue, the event crosses the shadow boundary and continues to the host element. - Light DOM Traversal: The path continues upward from
the host element through its ancestor elements in the light DOM,
reaching
documentand finallywindow.
// 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:
- Composed Events (
composed: true): Most standard UI events (such asclick,touchstart,keydown) are composed. They cross shadow boundaries, allowingcomposedPath()to include both internal shadow nodes and external ancestors. - Uncomposed Events (
composed: false): Certain events (such asmouseenter,mouseleave,load, or custom events wherecomposedis explicitly set tofalse) cannot cross shadow boundaries. For these events, propagation stops at theShadowRoot, andcomposedPath()only contains nodes within the local shadow tree.
Open vs. Closed Shadow Roots
The visibility of nodes within composedPath() is
strictly determined by the mode of the shadow root:
- Open Shadow DOM (
mode: 'open'): When a shadow root is created with{ mode: 'open' }, the traversal path includes all internal nodes and theShadowRootitself, regardless of where the listener is located. - Closed Shadow DOM (
mode: 'closed'): When a shadow root is created with{ mode: 'closed' }, the browser hides internal nodes from listeners defined outside the shadow tree. IfcomposedPath()is called from a listener attached to an outer ancestor, the internal shadow nodes and the closed shadow root are excluded from the array; the path will appear to start directly at the host element. However, ifcomposedPath()is called inside the closed shadow tree itself, the internal nodes remain visible in the returned array.
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.