Skip to content
sang.id.vn
Go back

When You Actually Need capture: true in addEventListener

Tiếng Việt

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().


Share this post on:

Previous Post
The float and fixed Positioning Trap in contenteditable
Next Post
How I Optimized the Auto-Scheduling Feature