Event Bubbling

Event bubbling is the default propagation mechanism (sequence) in the DOM, where an event first occurs on a targets element (e.g. <button>), it first runs on that element, then bubbles up to its parent, then grandparent (e.g <div>), and so on up the DOM three , until it reaches the root (i.e. <html>).

Event bubbling allows the parent elements to handle events that originate from their descendants, nesting element. By attaching event listeners to these elements, you can respond to user interactions, for instance: you can add event listener to a <div> with a <button> in it, such that when the <button> inside got clicked, the <div> border changes colour.

Example Event Bubbling using Button

As an example: for the following code, you get event bubbling from <button> to its parent <div>, to the grandparent <div>, … , till the very top level root <html> element (that triggers their corresponding handleClick events).

2025-11-17T111910

2025-11-17T112308

(Source code: index-lightdom-basic.html)

Stop Propagation (Event Bubbling)

You can stop the propagation of events (bubbling) via calling the .stopPropagation() function. For instance, if in the handleClick function, we check if the current event’s element is container-level3 and if true stop the propagation, you’ll get the following:

2025-11-17T112738

(Source code: index-lightdom-stop-propegation.html)


Event Bubbling in Web Component

Event bubbling in web component are controlled by the bubbles and composed property of an event:

  • if an event have its bubbles property set to true, then the event will bubble inside the shadow root
  • if an event have its composed property (read only) being true then the event can bubble through shadow boundary, in another word, it will escape the shadow root into the outside DOM element (element outside the shadow root will see it)

Consider the following custom event triggered on button click:

1
2
3
4
5
shadowRoot.querySelector('button').addEventListener('click', (e) => {
    e.stopPropagation(); // prevent the click event from bubbling
    const custom_event = new CustomEvent('example_event_insideShadowDom', { bubbles:true, composed:false});
    shadowRoot.querySelector('button').dispatchEvent(custom_event); // trigger custom event (instead of click event)
});

It will bubble up inside the shadow root, but it will never cross the shadow boundary and fire the listener outside the shadow root. (i.e. html, body, and div containers outside the shadow root)

Example via Custom Event:

Two confetti button, one with composed property set to true, one set to false

2025-11-17T115739

2025-11-17T120100

(Source code: index-shadow-confetti-composed-true-false.html)

For Native Events:

In the above example we have created a custom event to manually configure the composed property, but for the pre-existing events for DOM elements, it is in fact a read-only property that cannot be configured, usually all UA-dispatched UI events (events that originate from the browser’s user input system and propagate through the DOM as standard, native UI events) such as click/touch/mouseover/copy/paste are composed (true). That is they will propagate through the shadow boundary.

But for other events that are not UA-dispatched, the composed property will be set to false, and the event will not bubble through the shadow boundary and trigger the listeners in the light dom. You can check if an event is composed (true) via the following:

1
2
3
4
5
document.querySelector("html").addEventListener("click", (e) => {
  console.log(e.composed);
  console.log(e.composedPath());
});
// (replace "click" with other events

(see more at this documentation: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed#examples)


Reference