Event Propagation
Events don't just fire on one element. They travel through the DOM.
Three Phases
Every DOM event goes through three phases.
Click on <button> inside <div> inside <body>:
// By default, addEventListener listens during BUBBLING:
div.addEventListener("click", handler); // phase 3
// To listen during CAPTURING:
div.addEventListener("click", handler, true); // phase 1
// or: { capture: true }
// Check which phase in the handler:
element.addEventListener("click", (e) => {
e.eventPhase; // 1=capturing, 2=target, 3=bubbling
});Bubbling
Events bubble UP from the target to the root.
// HTML:
// <div id="outer">
// <div id="inner">
// <button id="btn">Click me</button>
// </div>
// </div>
outer.addEventListener("click", () => console.log("outer"));
inner.addEventListener("click", () => console.log("inner"));
btn.addEventListener("click", () => console.log("btn"));
// Click the button → output:
// "btn" (target)
// "inner" (bubbles up)
// "outer" (bubbles up)
// The event "bubbles" from child to parent to grandparent...💡 Bubbling is why event delegation works — a click on a child element also fires on all its ancestors.
Capturing
Events travel DOWN from root to target before bubbling.
// Capture phase (down) + bubble phase (up):
outer.addEventListener("click", () => console.log("outer capture"), true);
inner.addEventListener("click", () => console.log("inner capture"), true);
btn.addEventListener("click", () => console.log("btn"));
inner.addEventListener("click", () => console.log("inner bubble"));
outer.addEventListener("click", () => console.log("outer bubble"));
// Click button → output:
// "outer capture" ↓ (capturing down)
// "inner capture" ↓
// "btn" ● (target)
// "inner bubble" ↑ (bubbling up)
// "outer bubble" ↑Capturing is rarely used. The main use case is intercepting events before they reach the target — like a global click handler that blocks certain actions.
stopPropagation
Stop the event from traveling further.
// stopPropagation — prevent bubbling:
btn.addEventListener("click", (e) => {
e.stopPropagation();
console.log("btn clicked");
// Parent handlers will NOT fire!
});
outer.addEventListener("click", () => {
console.log("This won't run if btn is clicked");
});
// stopImmediatePropagation — also stops OTHER handlers
// on the SAME element:
btn.addEventListener("click", (e) => {
e.stopImmediatePropagation();
console.log("First handler");
});
btn.addEventListener("click", () => {
console.log("This won't run either!");
});stopPropagation
Stops bubbling/capturing
Other same-element handlers still fire
stopImmediatePropagation
Stops ALL propagation
Even same-element handlers stop
Practical Use
When propagation knowledge matters.
// 1. Modal close — click outside to close:
overlay.addEventListener("click", () => closeModal());
modal.addEventListener("click", (e) => {
e.stopPropagation(); // don't close when clicking INSIDE modal
});
// 2. Dropdown menu — close on outside click:
document.addEventListener("click", (e) => {
if (!dropdown.contains(e.target)) {
dropdown.classList.add("hidden");
}
});
// 3. Prevent link navigation in a card:
card.addEventListener("click", () => openCard());
card.querySelector("a").addEventListener("click", (e) => {
e.stopPropagation(); // let link work normally
});FAQ
Common questions about event propagation.