Event propagation in JavaScript: capture, target and bubbling

An event crosses the tree in three phases: capture, target, bubbling. How to read eventPhase, stop the run and spot what never bubbles at all.
3 min read
Believemy logo

A click on a link sitting inside a card, itself inside a list, itself inside the page: the event does not concern the link alone. It travels through the whole lineage.

Understanding that trip explains most of the surprising behavior of an interface, from the menu closing too early to the form submitted twice.


Definition

The same stopPropagation, two outcomes depending on the phase

Click inside a box to aim at it, then check capture without changing anything else.

bubbling
bubbling
bubbling
target
Learn more
stopPropagation() called on
Actual order of the calls
1atargeteventPhase 2
2.cardbubblingeventPhase 3
3#listbubblingeventPhase 3
stopPropagation()
·pagebubblingnever called

event.target equals a on every line that runs. Only event.currentTarget changes: it equals the element of the line.

3 listeners called out of 4. After #list the run stops, and the event never reaches page.

The log is computed, not captured from real listeners: it applies the DOM rules to four nested elements. The box registers capture on every ancestor at once, where real code picks it listener by listener. Each box carries a single listener, which leaves aside stopImmediatePropagation, whose difference only shows when one element carries several.

Click a box, then turn capture on. The order of the ancestors flips, and that is the whole difference between the two listening modes.

Propagation is the trip an event takes through the document tree. It starts at the root, goes down to the element aimed at, then climbs back up to the root. Every listener met along the way is called.

PhaseDirectionevent.eventPhase
CaptureFrom the root toward the target1
TargetOn the target element itself2
BubblingFrom the target back to the root3

By default, addEventListener() registers for the bubbling phase. Capture requires the capture option, and stays rare.


The order of the calls

JAVASCRIPT
const list = document.querySelector("#list");
const link = list.querySelector("a");

list.addEventListener("click", () => console.log("list, capture"), true);
link.addEventListener("click", () => console.log("link"));
list.addEventListener("click", () => console.log("list, bubbling"));

// A click on the link prints, in this order:
// list, capture
// link
// list, bubbling

Bubbling explains why a listener placed on a container fires although the click aimed at a child. That is not a flaw, it is the very mechanism Event delegation is built on.


target and currentTarget

Two properties answer two different questions, and mixing them up is the most frequent mistake in this area.

  • event.target names the element actually aimed at, often the deepest one in the tree.
  • event.currentTarget names the element the running listener was placed on.

Inside a classic function, this equals currentTarget. Inside an Arrow function it does not, so the explicit property is then the only safe route.


Stopping the run

JAVASCRIPT
link.addEventListener("click", (event) => {
  event.stopPropagation();   // the list will never see it
  console.log("handled here");
});

stopPropagation keeps the event from traveling further up the tree, but lets the other listeners on the same element run. stopImmediatePropagation cuts those too. Neither of them cancels the default browser action, which belongs to preventDefault().

Good to know

Stopping propagation out of convenience often breaks code written elsewhere: a menu closing on an outside click, a usage measurement, a global shortcut. Keep that move for the cases where it is genuinely needed.


Frequently asked questions

Question

Do all events bubble?

No. focus, blur, mouseenter and mouseleave stay on their target. The first two have bubbling twins, focusin and focusout, designed for exactly this need. The event.bubbles property settles the question at run time.


Question

What is the capture phase really for?

For stepping in before the aimed element, therefore before any listener placed lower down. It serves to intercept a gesture globally, for instance to block interactions during a load, or to catch an event that never bubbles.


Question

Can the current phase be read?

Yes, event.eventPhase hands back 1, 2 or 3 depending on the phase under way. It is mostly a diagnostic tool: production code rarely branches on the phase, and relies instead on comparing target with currentTarget.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.