skip to content

After a DOM event is dispatched on an element, what three phases does it pass through, and in what order do capture-phase and bubble-phase listeners on its ancestors run?

level: middleimportance: must knowfreq 70%

answer

  1. down, at, then back up
  2. outermost first, then innermost first
  3. the target ignores the distinction
  4. not every event makes the return trip
  5. eventPhase is 1, 2, or 3

basics

~20 s

An event first travels down from window to the target's parent firing capture listeners, then fires at the target, then travels back up firing bubble listeners. Ancestor capture handlers run outermost-first; ancestor bubble handlers run innermost-first.

solid answer

~50 s

Dispatch walks a path that is computed up front: from `window` down through `document`, the `<html>` element and every ancestor to the target, then back out the same way. Phase one is capture — the browser descends the path and invokes only capture-registered listeners, outermost first. Phase two is at-target, where every listener on the target itself runs in registration order regardless of which phase it asked for. Phase three is bubbling — the browser ascends the path invoking bubble-registered listeners, innermost first — but only if the event bubbles; `focus`, `blur`, `mouseenter` and `mouseleave` do not. `event.eventPhase` reports which phase you are in as `Event.CAPTURING_PHASE` (1), `AT_TARGET` (2) or `BUBBLING_PHASE` (3). Delegation lives in phase three, which is why a container handler sees clicks from descendants that did not exist when it was attached.

code

javascript · 12 lines
javascript
const outer = document.createElement('div');
const inner = document.createElement('button');
outer.append(inner);
document.body.append(outer);

outer.addEventListener('click', (e) => console.log('outer capture', e.eventPhase), true);
outer.addEventListener('click', (e) => console.log('outer bubble', e.eventPhase));
inner.addEventListener('click', (e) => console.log('inner bubble-reg', e.eventPhase));
inner.addEventListener('click', (e) => console.log('inner capture-reg', e.eventPhase), true);

inner.click();
// outer capture 1 / inner bubble-reg 2 / inner capture-reg 2 / outer bubble 3

go deeper

for a junior

Memorize the three names in order — capture, target, bubble — and be able to say that a listener on a parent hears clicks from its children because events bubble upward.

for a middle

Explain the direction of each sweep, that at-target runs listeners in registration order regardless of the capture flag, and name events that do not bubble.

for a senior

Be ready to reason about ordering across a real component tree: why an ancestor capture handler always precedes a nested bubble handler, and how you delegate events that never bubble.

for a principal

Own the convention question: decide where in the tree cross-cutting concerns like analytics or focus management should listen, and what phase that choice implies for teams building components inside it.

## The path is decided before anything runs When the browser dispatches an event at a node, it first builds the propagation path: the chain of ancestors from that node outward, ending at `document` and then `window`. Everything that follows is a walk over this list. The list is fixed when dispatch begins, which has consequences later (mutating the tree mid-dispatch does not reroute the event). ## Phase 1 — capture The browser walks the path from the outside in — `window`, `document`, `<html>`, `<body>`, … down to the target's parent — and on each node invokes only the listeners registered for the capture phase. Nothing on the target itself runs yet. Capture exists so that an ancestor can observe or intervene *before* anything nearer the target has a chance to react. ## Phase 2 — at target The event reaches the node it was dispatched at. Here the phase distinction collapses: every listener on the target runs, capture-registered and bubble-registered alike, in the order they were registered. Registering with the capture flag does not make a listener on the target run earlier than a sibling listener on the same node. `event.eventPhase` is `2` (`Event.AT_TARGET`) during this window. ## Phase 3 — bubbling The browser retraces the path outward, invoking bubble-phase listeners: the target's parent first, then its parent, out to `document` and `window`. Innermost handlers run first. Bubbling is conditional. Every event carries a read-only `bubbles` flag, and when it is `false` the dispatch simply ends after the at-target phase. Common non-bubbling events: `focus`, `blur`, `mouseenter`, `mouseleave`, and `scroll` when fired on an element. Their bubbling counterparts exist for exactly this reason — `focusin` and `focusout` bubble, `mouseover` and `mouseout` bubble. ```javascript outer.addEventListener('click', () => console.log('outer capture'), true); outer.addEventListener('click', () => console.log('outer bubble')); inner.addEventListener('click', () => console.log('inner')); // click on inner logs: outer capture, inner, outer bubble ``` ## Reading the phase at runtime `event.eventPhase` is an integer with named constants on the `Event` interface: `NONE` (0) before or after dispatch, `CAPTURING_PHASE` (1), `AT_TARGET` (2), `BUBBLING_PHASE` (3). It is occasionally useful in a generic handler that is registered for both phases and must behave differently in each, though most code infers the phase from where it registered. ## Why this is the delegation question in disguise A handler on a container sees descendant clicks because those clicks bubble through it. The container did not need to exist at the same time as the descendant, and does not need to know how many descendants there are — the path is computed at dispatch time from the tree as it stands then. That is the whole mechanism, and it is why rows added after page load are handled without re-attaching anything. It is also why delegating focus needs care: `focus` does not bubble, so a container listener never sees it. The two standard answers are to use `focusin`/`focusout`, which do bubble, or to register the container listener for the capture phase, since capture visits the container on the way down even for a non-bubbling event. ## Ordering pitfalls worth knowing - Two listeners for the same phase on the same node run in registration order. There is no priority system. - Capture on an ancestor always beats bubble on a nearer ancestor, because the entire capture sweep finishes before the at-target phase begins. - `dispatchEvent` runs the whole path synchronously: when the call returns, every listener has already executed. ## Saying it well Name the three phases in order, state that ancestor capture runs outermost-first and ancestor bubble innermost-first, note that at-target ignores the distinction, and add that some events do not bubble at all. That last point is what separates a memorized answer from one grounded in real debugging.

  • Which common DOM events do not bubble, and what do you use instead when you need to delegate them?
    `focus`, `blur`, `mouseenter` and `mouseleave` do not bubble, and `scroll` does not bubble when fired on an element. For focus, the bubbling pair `focusin`/`focusout` exists; for pointer enter/leave, `mouseover`/`mouseout` bubble. The other option is to register the container listener for the capture phase, which visits it on the way down regardless.
  • Two listeners are registered on the same element for the same event, one with the capture flag and one without. In what order do they run when that element is the target?
    In registration order. At the at-target phase the capture flag is ignored, so the flag buys no priority on the target itself. It only matters for nodes that are ancestors of the target, where capture-registered listeners run during the descent and bubble-registered ones during the ascent.
  • Is dispatch synchronous, and what does that mean for code after a dispatchEvent call?
    It is fully synchronous. `dispatchEvent` walks the entire path and runs every matching listener before returning, so the line after the call executes with all handler side effects already applied. Its return value is `false` if any listener called `preventDefault()` on a cancelable event.

saying these in an interview costs you the question

  • Saying events only bubble and capture is legacy
  • Claiming the capture flag makes a listener on the target run first
  • Assuming every DOM event bubbles to document
  • Believing bubbling reaches document but never window
  • Thinking the propagation path is recomputed at each step

context