Event Composed Path (`composedPath()`)

`Event.composedPath()` returns the ordered chain of `EventTarget`s through which an event is dispatched, from the originating target toward the top-level event context. It is especially useful when event propagation crosses shadow DOM boundaries.

An event travels through a propagation path that can include capture, target, and bubble phases. `event.composedPath()` exposes that path as an array, with the originating target appearing first in the returned sequence.

Shadow DOM can change what outside code observes about event propagation. For composed events, the path can include nodes inside a shadow tree as well as the shadow host; for events that are not composed, the path does not cross a shadow boundary in the same way.

The method is useful when `event.target` is not enough to understand where an event originated or which nested elements participated in dispatch. This commonly matters for event delegation, Web Components, and debugging interactions across shadow roots.

The returned array describes the dispatch path for that event; it is not a live collection that should be treated as the DOM itself. Code should also distinguish the event's propagation path from the DOM ancestry that can be obtained with APIs such as `parentNode`.

Whether an event crosses a shadow boundary depends on its `composed` behavior. Built-in events differ in this regard, while custom events can explicitly set `composed` when they are constructed.

Key points

Interview questions

What is the difference between `event.target` and `event.composedPath()` when Shadow DOM is involved?

`event.target` is subject to event retargeting, so code outside a shadow tree may see the shadow host rather than the internal node that initiated the event. `composedPath()` provides the dispatch path that the event exposes, including internal nodes when the event is composed and the context permits them to be observed. This distinction is important for Web Component event APIs and delegated handlers. The correct choice depends on whether the code needs the listener-relative target or the event's propagation route.

What does `event.composedPath()` return?

It returns an array representing the event's propagation path. The event's originating target is normally at the beginning of that array. The path can contain ancestors and, for composed events, entries associated with shadow DOM boundaries. It is useful when you need more context than `event.target` provides.

Why would you use `composedPath()` in event delegation?

Event delegation often handles events from many descendants through one listener. `event.target` tells you the target exposed at that listener, but the composed path lets you inspect the sequence of targets and ancestors involved in dispatch. This is particularly useful when components use shadow DOM. A delegated handler can search the path for an element matching a condition instead of assuming ordinary DOM ancestry.

How does shadow DOM affect `composedPath()`?

Shadow DOM introduces encapsulation boundaries into event dispatch. For a composed event, the event can propagate across a shadow boundary, and its composed path can contain nodes from the shadow tree along with the host and surrounding context. For a non-composed event, propagation does not cross the shadow boundary in the same way. Therefore, code that depends on the full path should consider the event's `composed` property.

How would you design a custom event that should cross a Web Component's shadow boundary?

Create the custom event with the `composed` option enabled, typically alongside `bubbles` when the event should propagate through ancestor listeners. For example, `new CustomEvent("change", { bubbles: true, composed: true })` allows the event to bubble across the shadow boundary. Consumers can then observe the event outside the component, while retargeting still affects the externally observed `target`. The component should expose only the event information that its public API intends to make observable.