JavaScript Interview Prep
DOM & Browser APIs

Event Bubbling vs Capturing

The Three Phases of Propagation

LinkedIn Hook

"Why does my parent's click handler fire when the user clicked a button inside it?"

Every junior developer asks this eventually. The answer is event propagation — a three-phase journey every DOM event makes, whether you want it to or not.

Capture phase goes DOWN. Target phase is where you clicked. Bubble phase comes back UP. Miss one and your modal won't close, or worse — it will close when it shouldn't.

In this lesson you'll trace the exact six-step propagation order on a real nested-div example, learn when to reach for stopPropagation vs stopImmediatePropagation, and see how to close an overlay on backdrop click without eating the click inside the modal.

Read the full lesson -> [link]

#JavaScript #EventPropagation #Frontend #InterviewPrep #WebDevelopment #DOM #CodingInterview


Event Bubbling vs Capturing thumbnail


What You'll Learn

  • The exact order of the capture, target, and bubble phases — with a six-step trace
  • The real difference between stopPropagation and stopImmediatePropagation
  • Practical patterns: backdrop-only-close modals using target === currentTarget

Stone in Water

When an event fires on an element, it doesn't just happen there. It travels through the DOM in three phases — like dropping a stone in water (capturing down), hitting the bottom (target), then the ripples coming back up (bubbling).

The Three Phases

  1. Capture Phase — The event travels DOWN from window -> document -> html -> body -> ... -> parent of target.
  2. Target Phase — The event reaches the actual element that was clicked.
  3. Bubble Phase — The event travels back UP from the target -> parent -> ... -> body -> html -> document -> window.

Nested Div Click Example — Exact Propagation Order

<div id="grandparent">
  <div id="parent">
    <div id="child">Click me!</div>
  </div>
</div>
const gp = document.getElementById("grandparent");
const p = document.getElementById("parent");
const c = document.getElementById("child");

// Bubble phase listeners (default)
gp.addEventListener("click", () => console.log("Grandparent - BUBBLE"));
p.addEventListener("click", () => console.log("Parent - BUBBLE"));
c.addEventListener("click", () => console.log("Child - TARGET/BUBBLE"));

// Capture phase listeners (useCapture = true)
gp.addEventListener("click", () => console.log("Grandparent - CAPTURE"), true);
p.addEventListener("click", () => console.log("Parent - CAPTURE"), true);
c.addEventListener("click", () => console.log("Child - TARGET/CAPTURE"), true);

// Click on #child — exact output order:
// 1. "Grandparent - CAPTURE"    (capture phase, going DOWN)
// 2. "Parent - CAPTURE"          (capture phase, going DOWN)
// 3. "Child - TARGET/CAPTURE"    (target phase)
// 4. "Child - TARGET/BUBBLE"     (target phase)
// 5. "Parent - BUBBLE"           (bubble phase, going UP)
// 6. "Grandparent - BUBBLE"      (bubble phase, going UP)

stopPropagation vs stopImmediatePropagation

const parent = document.getElementById("parent");
const child = document.getElementById("child");

// stopPropagation — stops event from reaching ancestors
child.addEventListener("click", function(e) {
  console.log("Child handler 1");
  e.stopPropagation();
  // Parent's listener will NOT fire
  // But other listeners on THIS element still fire
});

child.addEventListener("click", function(e) {
  console.log("Child handler 2"); // This STILL fires
});

parent.addEventListener("click", function() {
  console.log("Parent"); // This does NOT fire
});
// Click child -> "Child handler 1", "Child handler 2"


// stopImmediatePropagation — stops everything, even sibling listeners
child.addEventListener("click", function(e) {
  console.log("Child handler A");
  e.stopImmediatePropagation();
});

child.addEventListener("click", function(e) {
  console.log("Child handler B"); // Does NOT fire
});
// Click child -> "Child handler A" only

event.target vs event.currentTarget

document.getElementById("parent").addEventListener("click", function(e) {
  console.log("target:", e.target.id);         // the actual clicked element
  console.log("currentTarget:", e.currentTarget.id); // always "parent"
});

// Click on #child:
// target: "child"          (what was clicked)
// currentTarget: "parent"  (where the listener lives)

// Click on #parent directly:
// target: "parent"
// currentTarget: "parent"  (same element)

Practical Example: Preventing Close on Inner Click

// Modal that closes when clicking overlay, but NOT inner content
const overlay = document.getElementById("modal-overlay");
const modal = document.getElementById("modal-content");

overlay.addEventListener("click", function(e) {
  // Only close if the overlay itself was clicked
  if (e.target === overlay) {
    overlay.style.display = "none";
  }
  // Clicks on modal content bubble up but e.target !== overlay
});

// Alternative using stopPropagation
modal.addEventListener("click", function(e) {
  e.stopPropagation(); // prevent click from reaching overlay
});

Event Bubbling vs Capturing visual 1


Common Mistakes

  • Reaching for stopPropagation as a default habit — it hides events from analytics, modal managers, and delegation handlers that rely on bubbling. Prefer e.target === e.currentTarget checks when possible.
  • Forgetting that stopPropagation does NOT stop other listeners on the same element — only stopImmediatePropagation does. If you register two click handlers on the same node, the first calling stopPropagation won't block the second.
  • Registering a listener with { capture: true } and trying to remove it with removeEventListener(type, fn) without the same capture flag — removal silently fails because capture/bubble listeners are tracked separately.

Interview Questions

Q: What are the three phases of event propagation?

Capture (event travels from window down to the target's parent), Target (event reaches the clicked element), and Bubble (event travels from the target back up to window). Most listeners fire in the bubble phase by default.

Q: What is the difference between stopPropagation and stopImmediatePropagation?

stopPropagation() prevents the event from reaching ancestor or descendant elements (depending on phase), but other listeners on the SAME element still fire. stopImmediatePropagation() stops everything — no more listeners on the same element and no propagation.

Q: How do you listen in the capture phase?

Pass true as the third argument to addEventListener, or pass { capture: true } in the options object: el.addEventListener("click", handler, true).

Q: What is the default phase for addEventListener?

Bubble phase. Pass true or { capture: true } as the third argument to listen during capture instead.

Q: Given nested grandparent/parent/child with both capture and bubble listeners, what's the log order when the child is clicked?

Grandparent CAPTURE -> Parent CAPTURE -> Child CAPTURE (target) -> Child BUBBLE (target) -> Parent BUBBLE -> Grandparent BUBBLE. Six lines in that exact order.


Quick Reference — Cheat Sheet

EVENT PROPAGATION — QUICK MAP

Three phases (every event, in order):
  1. CAPTURE  window -> ... -> target.parent   (going DOWN)
  2. TARGET   on the clicked element
  3. BUBBLE   target -> ... -> window          (going UP)

Register per phase:
  el.addEventListener("click", fn)         -> bubble (default)
  el.addEventListener("click", fn, true)   -> capture
  el.addEventListener("click", fn, { capture: true })

Stopping:
  e.stopPropagation()
    -> halts ancestors / descendants
    -> SAME element's other listeners still run
  e.stopImmediatePropagation()
    -> halts everything, including siblings on same element

Targets:
  e.target         -> actually clicked node
  e.currentTarget  -> node that owns the listener

Pattern — close overlay only on backdrop:
  overlay.addEventListener("click", e => {
    if (e.target === e.currentTarget) close();
  });

Previous: Event Handling & Event Delegation Next: localStorage, sessionStorage, Cookies


This is Lesson 11.3 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page