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.