Event propagation in JavaScript: bubbling vs 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`.
Closely related to [[event-delegation-and-bubbling]] — this question focuses specifically on phases.
Three phases
document → html → body → div.card → button (CAPTURE)
↓
TARGET
↓
button → div.card → body → html → document (BUBBLE)When you click the button:
- Capture: event walks from document down to the target, firing capture-phase listeners along the way.
- Target: listeners on the target fire (both capture and bubble).
- Bubble: event walks back up, firing bubble-phase listeners.
Listening in each phase
el.addEventListener("click", fn); // bubble (default)
el.addEventListener("click", fn, { capture: true }); // capture
el.addEventListener("click", fn, true); // legacy syntax: capture: trueStopping propagation
| Method | Effect |
|---|---|
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
document.addEventListener("click", (e) => {
if (!popoverEl.contains(e.target)) close();
}, { capture: true }); // capture catches before child stopPropagationBind 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.