Learning Objectives
- Name the three phases: capture (document → target), target, bubble (target → document)
- Choose the phase a listener fires in with
addEventListener's third argument - Read
event.eventPhaseto see which leg of the journey a listener caught - Halt further travel with
stopPropagation()— and distinguish it frompreventDefault()
Down, land, up
When an event fires on a nested element, the browser first walks the tree downward from the document to the target — the capture phase — then arrives at the target itself, then walks back up to the document — the bubble phase. Listeners default to the bubble leg; passing true (or { capture: true }) as the third argument moves them to the capture leg:
Inside the handler, eventPhase reports which leg this listener caught: 1 for capture, 2 at the target, 3 for bubble. The demo's log line is exactly that check:
Why does app code default to bubbling? Because you usually care that the thing itself was clicked, and want ancestors informed afterward. Capture runs ancestors first, before the target has said a word — useful for global concerns like analytics or dismissing popups, and best used sparingly.
Live demo
Three nested boxes — outer, middle, inner — each with a click listener. Click the inner box and read the log: in bubble mode the order is inner → middle → outer; switch to capture and it reverses. Check inner.stopPropagation() and watch the journey end early — but note which direction it ends in each mode. The inner box is a real control (role="button", tabindex="0"), so Enter and Space click it from the keyboard too.
One implementation detail worth stealing: to switch phases the demo must shed its old listeners, and rather than bookkeeping every handler reference it clones each box and replaces it — el.replaceWith(el.cloneNode(true)) — because a clone carries markup but not listeners. A blunt tool, but an honest demonstration of where listeners actually live.
What stopPropagation does not do
stopPropagation() ends the event's travel through the tree — no further elements hear it. It does not cancel the default action; a link still navigates. preventDefault() cancels the default action — and does not slow the event's travel at all. They are independent, and needing both means calling both.Reach for stopPropagation() rarely. Code that silences events breaks any ancestor legitimately listening for them — including the delegation pattern you are about to build.
Next Steps
Bubbling is not trivia — it is infrastructure. Because every click on every child passes through the parent, the parent can handle all of them. In Step 4: Event delegation:
- Put one listener on a container instead of one per child
- Identify the real target with
e.target.closest() - Dispatch actions from
data-actionattributes