Event Bubbling and Capturing
Understand the three phases of DOM event propagation — capturing, target, and bubbling — and correctly use stopPropagation, preventDefault, and stopImmediatePropagation.
When you click a button nested inside several <div> elements, that click does not just happen on the button in isolation. The browser dispatches the event along a full path through the DOM tree, from the window down to the target and back up again, giving every ancestor element a chance to react. Understanding this path — called event propagation — is essential for writing handlers that behave the way you expect.
The three phases of event dispatch
Every DOM event travels through three phases, in this order: the capturing phase, where the event moves down from window through document, through each ancestor, toward the target; the target phase, where the event reaches the exact element that was interacted with; and the bubbling phase, where the event then travels back up, from the target through each ancestor, all the way to window.
By default, addEventListener registers a listener for the bubbling phase (and the target phase, when the listener is on the target itself). To listen during the capturing phase instead, pass true (or { capture: true }) as the third argument.
Bubbling in practice
Bubbling means that clicking a deeply nested child element also triggers click listeners on every one of its ancestors, in order from the target outward, unless something stops that propagation. This is not a special feature you opt into — it is simply how most events (click, input, submit, and many others) work by default.
This is the mechanism that makes event delegation possible: instead of attaching a listener to every individual child, you can attach one listener to a shared ancestor and let bubbling carry the event up to it.
Capturing
Capturing is the mirror image of bubbling: instead of listening as the event travels outward from the target, a capturing listener fires as the event travels inward, before the target itself has even been reached. This is used far less often than bubbling, but it is valuable when an ancestor needs the first opportunity to intercept an event — for example, to close a menu before any inner click handler has a chance to run.
stopPropagation vs preventDefault
These two methods on the event object are easy to confuse but do completely unrelated things. event.stopPropagation() stops the event from continuing to travel through the capturing/bubbling path — it prevents other listeners (on ancestor or descendant elements) from being notified, but it does not cancel the browser's default behavior.
event.preventDefault() cancels the browser's built-in default action for that event — for example, stopping a link from navigating, a form from submitting, or a checkbox from toggling — but it does not stop the event from continuing to bubble or capture. The two are often used together, but they solve different problems and neither one implies the other.
stopImmediatePropagation
event.stopImmediatePropagation() does everything stopPropagation() does, plus one more thing: it also prevents any other listeners registered on that same element for that same event from running, even ones registered after it. This is useful when multiple independent listeners share an element and one of them needs to be the absolute last word.
Code Examples
All three listeners are registered for the default bubbling phase, so clicking the innermost button fires the target's own listener first, then bubbles outward through inner and finally outer. Expected result: target inner (bubble) outer (bubble)
With no capture option, listeners fire from the target outward: target → inner → outer.
Registering the outer listeners with capture: true (the shorthand true) makes them fire during the downward capturing phase, before the target's own bubbling-phase listener runs. Expected result: outer (capture) inner (capture) target
Capturing listeners fire from the outside in: outer → inner → target.
Clicking the link cancels the browser's default navigation (preventDefault) AND stops the click from bubbling up to the nav's handler (stopPropagation), so the mobile menu does not close as a side effect of confirming an external link. Expected result: link handler: showing confirmation dialog instead of navigating preventDefault() cancels the browser's default action; stopPropagation() independently stops the event from reaching ancestor listeners — using one does not imply the other.
Common mistake: Calling only preventDefault() here would stop the navigation but the click would still bubble to nav's handler, unexpectedly closing the mobile menu too.
const outer = document.querySelector("#outer");
const inner = document.querySelector("#inner");
const target = document.querySelector("#target");
outer.addEventListener("click", () => console.log("outer (bubble)"));
inner.addEventListener("click", () => console.log("inner (bubble)"));
target.addEventListener("click", () => console.log("target"));
// Clicking the button:const outer = document.querySelector("#outer");
const inner = document.querySelector("#inner");
const target = document.querySelector("#target");
outer.addEventListener("click", () => console.log("outer (capture)"), true);
inner.addEventListener("click", () => console.log("inner (capture)"), true);
target.addEventListener("click", () => console.log("target"));
// Clicking the button:const link = document.querySelector("a.external-warning");
const nav = document.querySelector("nav");
nav.addEventListener("click", function () {
console.log("nav handler: closing mobile menu");
});
link.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
console.log("link handler: showing confirmation dialog instead of navigating");
});The event propagation path
A click on <button id="target"> first travels DOWN through window → document → #outer → #inner (capturing phase), fires once AT the button (target phase), then travels back UP through #inner → #outer → document → window (bubbling phase). A regular addEventListener listener on #outer only sees the event during the bubbling leg unless capture: true is used.
Window
│ 1. capturing (going down)
▼
Document
│
▼
<div id="outer">
│
▼
<div id="inner">
│
▼
<button id="target">◄── 2. TARGET phase (event fires here)
│
▲
<div id="inner">
│
▲
<div id="outer">
│
▲ 3. bubbling (going back up)
Document
│
▲
WindowImportant Rules
Every event travels: capturing phase (down) → target phase → bubbling phase (up), unless propagation is stopped.
addEventListener listens on the bubbling phase by default; pass true or { capture: true } to listen during capturing instead.
stopPropagation() stops further travel through the DOM but does NOT cancel the browser's default action.
preventDefault() cancels the browser's default action but does NOT stop the event from bubbling or capturing further.
stopImmediatePropagation() stops propagation AND blocks any other listeners on the same element/event from running.
Where this shows up
Modal dialogs are the classic bubbling example: a click listener on the full-screen overlay closes the modal, but a click inside the modal's content box must call stopPropagation() so that click does not bubble up to the overlay and close the modal the user is actively interacting with. Custom dropdown menus, tab interfaces, and nested clickable cards all rely on the same careful use of propagation control.
// Assuming preventDefault() also stops a click from bubbling to a parent handler link.addEventListener("click", (e) => e.preventDefault());
link.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); });
preventDefault() only cancels the element's own default browser behavior (like following a link). It has no effect on whether the event continues to bubble up to ancestor listeners — that requires stopPropagation() separately.
[Multiple Choice] In which order do default (bubbling-phase) listeners on #target, #inner, and #outer fire when #target is clicked, given #target is nested inside #inner inside #outer? A. #outer, then #inner, then #target B. #target, then #inner, then #outer C. All three fire simultaneously D. Only #target fires; ancestors never see the event
B. #target, then #inner, then #outer — Bubbling-phase listeners fire from the target outward: the target's own listener runs first, then each ancestor in turn, ending with the outermost element.
[True / False] Calling event.preventDefault() automatically stops the event from bubbling to parent elements. A. True B. False
B. False — False. preventDefault() only cancels the browser's default action for that event; it has no effect on propagation. Stopping propagation requires calling stopPropagation() separately.
Given a modal overlay <div id="overlay"> containing a <div id="modal-content">, write listeners so that clicking anywhere on the overlay closes the modal, but clicking inside modal-content does not.
const overlay = document.querySelector("#overlay"); const content = document.querySelector("#modal-content"); overlay.addEventListener("click", function () { overlay.classList.remove("open"); }); content.addEventListener("click", function (event) { event.stopPropagation(); });
Attach the close logic to #overlay, and call stopPropagation() inside a click listener on #modal-content.