Most tutorials explain event capturing vs bubbling in theory but never show when you’d actually need it. Here’s a real case where capture: true is the right answer.
DOM events propagate in three phases:
CAPTURE (top-down)
document → div.container → button
TARGET
button
BUBBLE (bottom-up)
document ← div.container ← button
By default, addEventListener('click', fn) registers on the bubble phase. To register on the capture phase:
element.addEventListener("click", fn, { capture: true });
The Problem
A classic “click outside to close” dropdown. An inner button calls stopPropagation(), which prevents the click from bubbling up to document, so the close handler never fires.
<div id="dropdown-container">
<button id="toggle">Toggle Dropdown</button>
<div id="dropdown" class="hidden">
<p>Dropdown content</p>
<button id="inner-action">Action</button>
</div>
</div>
const toggle = document.getElementById("toggle");
const dropdown = document.getElementById("dropdown");
const container = document.getElementById("dropdown-container");
toggle.addEventListener("click", () => {
dropdown.classList.toggle("hidden");
});
// Close dropdown when clicking outside
document.addEventListener("click", e => {
if (!container.contains(e.target)) {
dropdown.classList.add("hidden");
}
});
// This stops the click from reaching document
document.getElementById("inner-action").addEventListener("click", e => {
e.stopPropagation();
});
The Fix
Use capture: true on the document listener. The capture phase runs top-down before any bubble-phase stopPropagation() can interfere:
document.addEventListener(
"click",
e => {
if (!container.contains(e.target)) {
dropdown.classList.add("hidden");
}
},
true // capture phase
);
Notes
When removing a capture-phase listener, you must pass the same flag:
document.addEventListener("click", handler, true);
document.removeEventListener("click", handler, true);
Calling stopPropagation() in the capture phase will prevent the event from reaching any child elements.
Other use cases: focus/blur delegation (these events don’t bubble), analytics/logging that must intercept every click regardless of stopPropagation().