When an event fires on an element nested inside other elements, it doesn't just affect that one element — it travels through the DOM tree in two phases: capturing (top down, from the root toward the target) and bubbling (bottom up, from the target back toward the root). Understanding this travel path explains a lot of behavior that otherwise seems confusing, like a click handler on a parent firing even when you clicked a child element.
A Bubbling Example
<!-- HTML -->
<div id="outer">
Outer
<div id="inner">
Inner
<button id="btn">Click Me</button>
</div>
</div>
document.getElementById("outer").addEventListener("click", () =>
console.log("Outer div clicked")
);
document.getElementById("inner").addEventListener("click", () =>
console.log("Inner div clicked")
);
document.getElementById("btn").addEventListener("click", () =>
console.log("Button clicked")
);
Clicking the button doesn't just run the button's own listener — by default, the click event then bubbles up through every ancestor, firing their listeners too.
Output (after clicking the button):
Button clicked
Inner div clicked
Outer div clicked
Notice the order: the deepest element (the actual click target) runs first, then its listener bubbles up to each parent in turn, from the inside out.
Capturing: The Other Direction
Capturing is the opposite — top-down, from the outermost ancestor toward the target — and happens before bubbling. It's rarely used, but you can opt into it by passing true (or { capture: true }) as a third argument to addEventListener:
document.getElementById("outer").addEventListener(
"click",
() => console.log("Outer — capturing phase"),
true // enables capturing for this listener
);
Stopping Propagation
Sometimes you want an event to affect only the element it happened on, without triggering parent listeners. event.stopPropagation() does exactly that:
document.getElementById("btn").addEventListener("click", (event) => {
console.log("Button clicked");
event.stopPropagation(); // the click stops here — parents never hear about it
});
With this in place, clicking the button now only logs "Button clicked" — the "Inner div clicked" and "Outer div clicked" lines never run. This matters most in real UIs when, for example, a modal's close button shouldn't also trigger a click listener on the overlay behind it. It's closely related to the broader topic of event listeners, which covers how listeners are attached in the first place.
