CodingNic

Events

Event Bubbling and Capturing

Events 20 min read

Event Bubbling and Capturing

Objectives

By the end of this chapter, you should be able to:

  • Explain event bubbling and predict the order listeners fire in on nested elements
  • Stop an event from bubbling further with .stopPropagation()
  • Explain what the capture phase is and when you’d use it

💡 Why this matters: When elements are nested, a single click can trigger listeners on several of them. Knowing the order they fire in avoids a whole category of confusing bugs, and is the foundation for delegation, next lesson.

Bubbling: Events Travel Upward

When an event happens on an element, it doesn’t just fire that element’s listener. It bubbles: after running on the target, it fires again on the target’s parent, then that parent’s parent, all the way up to document.

Given this HTML:

text
<div id="outer">
  <div id="inner">
    <button id="innerBtn">Click</button>
  </div>
</div>
javascript
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
const innerBtn = document.getElementById("innerBtn");

outer.addEventListener("click", () => console.log("outer"));
inner.addEventListener("click", () => console.log("inner"));
innerBtn.addEventListener("click", () => console.log("innerBtn"));

innerBtn.click();
// innerBtn
// inner
// outer

Clicking the button fires all three listeners, in this order: the target first (innerBtn), then each ancestor going outward (inner, then outer). This is the default behavior for most events, including click.

Stopping the Bubble: .stopPropagation()

Sometimes you want an event to fire on the element it happened on and go no further. event.stopPropagation(), called inside a listener, prevents the event from continuing to bubble to ancestors.

javascript
inner.addEventListener("click", (event) => {
  console.log("inner");
  event.stopPropagation();
});

innerBtn.click();
// innerBtn
// inner

With .stopPropagation() added to inner’s listener, outer’s listener never runs, the event stops bubbling once it reaches inner. Reach for this when a click on something nested (like a delete button inside a clickable card) shouldn’t also trigger the card’s own click behavior.

The Capture Phase

There’s actually a phase that happens before bubbling: capturing. The event travels from document down to the target first, then bubbles back up. By default, listeners only run during the bubble phase. Passing { capture: true } as a third argument makes a listener run during the capture phase instead.

javascript
outer.addEventListener("click", () => console.log("outer capture"), { capture: true });
inner.addEventListener("click", () => console.log("inner capture"), { capture: true });
innerBtn.addEventListener("click", () => console.log("innerBtn bubble"));

innerBtn.click();
// outer capture
// inner capture
// innerBtn bubble

The capture listeners fire first, on the way down (outer before inner, the opposite order from bubbling), then the regular bubble-phase listener fires on the target itself. In practice, capturing is rare, almost all real code uses the default bubble phase. It’s worth knowing it exists so { capture: true } isn’t a mystery if you run into it.

Try It

Starter HTML for all three exercises:

text
<div id="grandparent">
  <div id="parent">
    <div id="child">Click me</div>
  </div>
</div>

<div id="card">
  <button id="delete">Delete</button>
</div>
  1. Add a click listener to #grandparent, #parent, and #child that each log their own id. Click #child and predict the order before running it.
  2. Add event.stopPropagation() to #parent’s listener. Click #child again and confirm #grandparent’s listener no longer runs.
  3. Add a click listener to #card that logs "card clicked", and one to #delete that logs "delete clicked" and calls .stopPropagation(). Click #delete and confirm only "delete clicked" logs.

Recap

  • Events bubble: after firing on the target, they fire again on each ancestor going outward, up to document.
  • event.stopPropagation() stops an event from bubbling past the element that called it.
  • Capturing is a phase before bubbling, traveling from document down to the target. Listeners only join it with { capture: true }, most code never needs this.

Next lesson: event delegation, using bubbling on purpose to handle elements that don’t exist yet.