When working with Web Components and Shadow DOM, you might notice that UI events such as mouseover and mouseout don’t always bubble up to the outside DOM tree as expected. This can be confusing, especially since MDN states that “All UA-dispatched UI events are composed (click/touch/mouseover/copy/paste, etc.).”
My Experience Debugging Shadow DOM Events
Recently, I was building a custom cursor for my blog and wanted to listen for mouseover and mouseout events from the outside — specifically on the document or body. I expected these events to bubble up, since MDN says they’re composed. But in practice, I couldn’t match the event target to the internal tag I cared about.
After some digging (and reading this excellent guide), I realized:
- Even though UI events are composed, as they cross the shadow boundary, their
event.targetis retargeted to the shadow host. So, from outside, you only see the host, not the internal element. - This broke my logic, since I wanted to know which internal element triggered the event.
How I Solved It
To forward events with all their info, I now re-dispatch them from my custom element like this:
// Inside my custom element
const forwardEvent = (event: Event) => {
const forwarded = new event.constructor(event.type, {
bubbles: true,
composed: true,
cancelable: event.cancelable,
detail: event.detail ?? undefined,
});
this.dispatchEvent(forwarded);
};
this.shadowRoot.addEventListener('mouseover', forwardEvent);
This lets outside listeners catch the event, but the target is still the host. To get the real internal element, I use event.composedPath():
document.addEventListener('mouseover', (event: Event) => {
const originalTarget = event.composedPath()[0];
// Now I can match the actual element inside the shadow DOM or traverse the path as needed (like I usually do with `originalTarget.closest('<selector>')`).
});
This trick finally let me do what I wanted: match the internal tag and handle the event properly from outside.
Takeaways
event.targetis retargeted to the shadow host when crossing shadow boundaries.- Use
event.composedPath()to access the true originating element inside shadow DOM. - Forward events with
{ bubbles: true, composed: true }for outside access.
If you’re struggling with event propagation in Shadow DOM, check the composed path!
References
- MDN: Event.composed
- MDN: Event.composedPath
- Shadow DOM and Event Propagation © Pierre-Marie Dartus