Frontend Labs
⋯

23. The event model: capturing, bubbling, delegation, passive listeners

DOM events travel through three phases: capturing (root → target), target, then bubbling (target → root) — this two-directional travel is what lets a listener on an ancestor react to events from descendants it doesn't even know exist yet.

// listener fires during the BUBBLING phase by default (3rd arg false/omitted)
document.body.addEventListener("click", (e) => console.log("bubble: body"));
// listener fires during the CAPTURING phase (3rd arg true)
document.body.addEventListener("click", (e) => console.log("capture: body"), true);

// clicking a deeply nested button logs:
// "capture: body"  (travels down first)
// ... then any listeners on the target itself ...
// "bubble: body"    (travels back up)

Event delegation exploits bubbling to attach ONE listener to a container instead of one per child — critical for lists with thousands of items or items added dynamically after the listener was set up:

// One listener handles clicks on ANY current or future <li>, no per-item binding needed
document.querySelector("ul").addEventListener("click", (e) => {
  const li = e.target.closest("li");
  if (li) console.log("clicked:", li.textContent);
});

Passive listeners tell the browser "this listener will never call preventDefault()", letting the browser start scrolling immediately instead of waiting to see if your touchstart/wheel handler cancels it — a measurable, real scroll-jank fix on mobile:

el.addEventListener("touchstart", handleTouch, { passive: true });

Generated from real-life-JS-guide.md · 8 Oct 2026 16:36