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:
<div id="outer">
<div id="inner">
<button id="innerBtn">Click</button>
</div>
</div>
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.
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.
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:
<div id="grandparent">
<div id="parent">
<div id="child">Click me</div>
</div>
</div>
<div id="card">
<button id="delete">Delete</button>
</div>
- Add a click listener to
#grandparent,#parent, and#childthat each log their own id. Click#childand predict the order before running it. - Add
event.stopPropagation()to#parent’s listener. Click#childagain and confirm#grandparent’s listener no longer runs. - Add a click listener to
#cardthat logs"card clicked", and one to#deletethat logs"delete clicked"and calls.stopPropagation(). Click#deleteand 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
documentdown 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.