Back to Browser Internals
Browser Internals
easy
mid

How does event propagation work in the DOM, and what is bubbling versus capturing?

Events propagate in three phases: capture (document → target), target, bubble (target → document). `addEventListener(fn)` defaults to bubble; pass `{ capture: true }` for capture. Stop with `stopPropagation()` (further ancestors only) or `stopImmediatePropagation()` (same-element siblings too). Some events don't bubble (`focus`, `blur`) — use `focusin`/`focusout`.

3 min read·~6 min to think through

Closely related to [[event-delegation-and-bubbling]] — this question focuses specifically on phases.

Three phases

ts
document → html → body → div.card → button   (CAPTURE)

                                         TARGET

button → div.card → body → html → document   (BUBBLE)

When you click the button:

  1. Capture: event walks from document down to the target, firing capture-phase listeners along the way.
  2. Target: listeners on the target fire (both capture and bubble).
  3. Bubble: event walks back up, firing bubble-phase listeners.

Listening in each phase

js
el.addEventListener("click", fn);                 // bubble (default)
el.addEventListener("click", fn, { capture: true }); // capture
el.addEventListener("click", fn, true);            // legacy syntax: capture: true

Stopping propagation

MethodEffect
event.stopPropagation()Stops further bubble or capture. Other listeners on the same element still fire.
event.stopImmediatePropagation()Halts everything, even sibling listeners on the same element.
event.preventDefault()Cancels the browser default action (form submit, link nav). Does not stop propagation.

Why use capture phase

Rarely needed, but useful when:

  • Intercepting before a child can cancel. A capture listener on the parent runs before the child's handler.
  • Logging / analytics wants to see all events without competing with child handlers.
  • Modal / overlay wants to catch clicks outside before they reach normal handlers.

Non-bubbling events

focus, blur, mouseenter, mouseleave don't bubble. Their bubbling equivalents:

  • focusin / focusout (instead of focus/blur).
  • mouseover / mouseout (instead of mouseenter/mouseleave).

Useful properties on the event

  • event.target — deepest element the event hit (can differ from currentTarget).
  • event.currentTarget — element the listener is bound to.
  • event.eventPhase — 1 (capture), 2 (target), 3 (bubble).
  • event.composedPath() — array of elements the event walks; useful with shadow DOM.

React notes

React's synthetic event system (since v17) delegates at the app root. onClick is bubble; onClickCapture is capture. e.stopPropagation() on a synthetic event only stops React listeners — native listeners further up still fire (and vice versa).

Example: outside-click for a popover

js
document.addEventListener("click", (e) => {
  if (!popoverEl.contains(e.target)) close();
}, { capture: true });   // capture catches before child stopPropagation

Bind on capture so child stopPropagation doesn't break dismiss.

Interview framing

"Three phases: capture from document down, target, bubble from target back up. addEventListener defaults to bubble; { capture: true } for capture. stopPropagation halts further ancestor handlers but not siblings on the same element; stopImmediatePropagation halts everything. preventDefault is unrelated — it cancels the browser's default action. Capture phase is useful for outside-click detection and analytics that need to see events before child cancels. Non-bubbling events (focus/blur) have bubbling equivalents (focusin/focusout) for delegation. React adds onClickCapture for capture-phase handlers."

Follow-up questions

  • When have you used capture in production?
  • What's the difference between stopPropagation and stopImmediatePropagation?
  • Why don't focus and blur bubble?

Common mistakes

  • Confusing preventDefault with stopPropagation.
  • Trying to delegate focus events.
  • Overusing stopPropagation, breaking ancestor handlers.

Performance considerations

  • Capture listeners run earlier — for very high-frequency events, avoid heavy work in either phase.

Edge cases

  • stopPropagation in React vs native.
  • Shadow DOM event retargeting.
  • composedPath for deep DOM trees.

Real-world examples

  • Outside-click hooks, modal/popover libraries, React's root delegation, Radix dismissable layer.

Senior engineer discussion

Seniors use capture rarely but knowingly, and explain how React's synthetic + native interplay can surprise people who mix UI libraries.

Related questions