Bubbling & capturing
Learn how browser events travel through nested elements, how target differs from currentTarget, and when stopPropagation helps or hurts.
- 01Trace the three phasesPredict capture, target, and bubble order for nested elements.
- 02Read the event objectUse
target,currentTarget,eventPhase, andcomposedPath()accurately. - 03Stop carefullyChoose between
stopPropagation,stopImmediatePropagation, andpreventDefault.
The trip an event takes
When you click a button, the browser does not only tell the button. It sends one event object on a round trip through the page tree. The trip has three parts: capture on the way in, target on the element that was actually hit, and bubble on the way back out.
That sounds fancy, but it explains a lot of everyday behavior: why a card can hear a click from a button inside it, why a menu can close when the document hears an outside click, and why a careless stopPropagation() can break code far away from the line that called it.
Imagine a courier carrying a package to an apartment. They enter the building, pass the lobby, go to the floor, and reach the apartment. After delivery, everyone hears the news in reverse order: apartment, floor, lobby. A click takes the same kind of trip through nested elements.
- In real life: Courier enters the lobby
- In JavaScript: Capture starts at the outside: window, document, page section
- In real life: Courier reaches the apartment
- In JavaScript: Target phase on the clicked element
- In real life: News travels back to the floor and lobby
- In JavaScript: Bubble phase climbs back through ancestors
- In real life: Any level can react
- In JavaScript: Any ancestor with a listener can run code
Where the analogy stops: Real buildings have one lobby and one staircase. Browser event paths can include shadow DOM and iframes, and composedPath() is the reliable way to inspect the exact path.
Event propagation is the browser sending one event object through ancestors on the way to the target, then through ancestors again on the way out.
Capture, target & bubble phases
STEP THROUGHA listener can join either side of the trip. The third argument to addEventListener chooses capture; leaving it out listens during target/bubble:
section.addEventListener("click", log, { capture: true });card.addEventListener("click", log, { capture: true });button.addEventListener("click", log, { capture: true }); button.addEventListener("click", log);card.addEventListener("click", log);section.addEventListener("click", log);For a click on the button, capture listeners run from outer to inner. Then the target’s own listeners run. In modern browsers, capture listeners registered on the target itself run before non-capturing target listeners; older browsers historically differed, so avoid code that depends on tricky same-target ordering. Finally, events whose bubbles property is true bubble from inner to outer.
| Phase | Direction | eventPhase | Listener option |
|---|---|---|---|
| Capture | window → document → ancestors → parent | 1 / CAPTURING_PHASE | { capture: true } or true |
| Target | the actual target element | 2 / AT_TARGET | capture listeners, then non-capturing listeners in modern browsers |
| Bubble | parent → ancestors → document → window | 3 / BUBBLING_PHASE | default listener option, when event.bubbles is true |
The model below is intentionally plain JavaScript data, not a DOM. It lets you replay the same order tests can verify in Node, then compare with the real browser visualizer in the next section.
Replay the event path from the pure model. Change the stop setting, then compare with the real DOM visualizer above.
script
const path = ["section", "card", "toolbar", "button", "icon"];const listeners = ["capture", "bubble"]; for (const node of path.slice(0, -1)) {} runListener("button", "target capture");runListener("button", "target bubble"); for (const node of ["toolbar", "card", "section"]) { runListener(node, "bubble");}Experiment: watch propagation in a real DOM
INTERACTIVENow try the browser’s real event system. The demo renders four nested boxes with React, then an effect attaches native DOM listeners to those existing nodes and removes them during cleanup. Clicking the star inside the button shows the exact order, plus eventPhase, target, and currentTarget on every line.
const section = document.querySelector("[data-box='section']");const card = document.querySelector("[data-box='card']");const toolbar = document.querySelector("[data-box='toolbar']");const button = document.querySelector("[data-box='button']"); section.addEventListener("click", log, { capture: true });section.addEventListener("click", log);button.addEventListener("click", log, { capture: true });button.addEventListener("click", log); function log(event) { console.log(event.eventPhase, event.target, event.currentTarget);}Click the star span inside the button. Capture listeners run top-down, then the target, then bubble listeners bottom-up.
Toggle capture off and the first half disappears. Toggle bubble off and only capture plus target listeners remain. Choose a stop point and watch later nodes stop hearing the event.
const section = document.querySelector("[data-box='section']");const card = document.querySelector("[data-box='card']");const toolbar = document.querySelector("[data-box='toolbar']");const button = document.querySelector("[data-box='button']"); section.addEventListener("click", log, { capture: true });section.addEventListener("click", log);button.addEventListener("click", log, { capture: true });button.addEventListener("click", log); function log(event) { console.log(event.eventPhase, event.target, event.currentTarget);}target vs currentTarget
event.target is where the event started: the deepest node the browser considers clicked. event.currentTarget is the element whose listener is running right now. In the visualizer, click the star span inside the button: logs from the section still have target as the span, but currentTarget as the section.
Nesting dolls make bubbling easy to picture. Poke the tiny doll in the center and every shell around it can feel the poke. The original poke point stays the same; the doll currently reacting changes.
- In real life: You poke the innermost doll
- In JavaScript:
event.targetis the deepest clicked span - In real life: Each outer doll feels it in turn
- In JavaScript: Each ancestor listener runs with its own
currentTarget - In real life: The outer doll is not the poke point
- In JavaScript:
currentTargetchanges;targetstays stable
Where the analogy stops: Real dolls do not have two phases. The analogy is only about one poke being noticed by containers around the poked thing.
| Property | Meaning | In the star-click demo |
|---|---|---|
event.target | Original element where the event began | the span with the star |
event.currentTarget | Element whose listener is currently running | section, card, toolbar, or button depending on the log line |
event.composedPath() | Array-like path the event travels | starts near the span and includes ancestors |
Inside a listener, prefer event.currentTarget when you mean “the element I attached this listener to.” Use event.target when you intentionally care about the descendant that was clicked.
stopPropagation and stopImmediatePropagation
COMPARESometimes a listener should stop the trip. A dialog might catch an inside click so a document-level outside-click handler does not close it. That is what event.stopPropagation() does: it stops the event from reaching further nodes. It does not stop other listeners already waiting on the current node.
Calling stopPropagation() is like closing the stairwell door after news reaches one floor. The news does not travel farther up or down the stairs, but people already in that room can still hear it. stopImmediatePropagation() is the stricter version: it silences the remaining people in the same room too.
- In real life: Close the stairwell door
- In JavaScript:
stopPropagation()stops the news going to other floors - In real life: People in the same room still hear it
- In JavaScript: Other listeners on the same node still run
- In real life: Silence everyone in the room too
- In JavaScript:
stopImmediatePropagation()skips remaining same-node listeners
Where the analogy stops: Events are not sound waves. The methods affect listener delivery only; they do not undo work that already ran and do not prevent default browser actions.
| Method | Stops later nodes? | Stops same-node listeners? | Stops default action? |
|---|---|---|---|
stopPropagation() | Yes | No | No |
stopImmediatePropagation() | Yes | Yes | No |
preventDefault() | No | No | Yes, when the event is cancelable |
Propagation is how outside-click handlers, analytics, keyboard shortcuts, and the next lesson’s Event delegation pattern work. Stop it only when you own both sides of the behavior and can explain what ancestor should not hear the event.
Events that do not bubble
REAL BROWSERCapture can observe any event that is dispatched through the tree, but only events with bubbles: true bubble. Useful pairs to remember are focus/blur versus focusin/focusout, and mouseenter/mouseleave versus mouseover/mouseout.
panel.addEventListener("focus", log, true);panel.addEventListener("focus", log);panel.addEventListener("focusin", log); panel.addEventListener("mouseenter", log, true);panel.addEventListener("mouseenter", log);panel.addEventListener("mouseover", log); function log(event) { console.log(event.type, event.bubbles);}Focus the input, tab away, and move the pointer through the card. Watch event.bubbles: capture can observe non-bubbling events, but ordinary bubble listeners on ancestors only hear bubbling ones.
event.bubbles property.clickfocusinmouseoverfocusblurmouseentermouseleavefocusout
Sort each event by the propagation behavior you usually rely on. The demo above lets you verify the browser's bubbles flag.
Where you will use this
Propagation is not trivia. It is the reason one listener can manage a whole region, and the reason a component can accidentally interfere with the page around it.
const menu = document.querySelector(".menu");const button = document.querySelector(".menu-button"); document.addEventListener("click", (event) => { const clickedInside = menu.contains(event.target) || button.contains(event.target); if (!clickedInside) menu.hidden = true;}); button.addEventListener("click", (event) => { event.stopPropagation(); menu.hidden = !menu.hidden;});The document listener relies on bubbling so outside clicks arrive. The button stops its own click because the button is not an outside click. In a larger app, you might avoid stopping and instead make the document listener check the path with composedPath().
section capturecard capturebutton capturebutton bubbletoolbar bubblesection bubble
Place each listener in the early capture group, at-target group, or later bubble group.
Common misconceptions
“Bubbling means the event object is copied.”
One event object travels. Its currentTarget and eventPhase change as different listeners run.
“Capture only happens for bubbling events.”
Capture can happen for dispatched events even when bubbles is false. The focus demo proves it.
“stopPropagation stops everything.”
It stops later nodes, not other listeners on the current node, and not default actions.
“target and currentTarget are usually interchangeable.”
They are equal only when the listener is running on the original target. Ancestor listeners see different values.
“If a parent hears a child click, the child’s handler failed.”
No. That is normal bubbling. It is often exactly what you want.
Practice: trace the trip
5 EXERCISESWhat number does event.eventPhase report during the bubble phase?
event.eventPhase is 3 during the bubbling phase. The constant name is Event.BUBBLING_PHASE.
If a learner clicks the icon <span> inside a button, what is event.target?
The star is a span inside the button, so event.target is that span. The button listener's currentTarget is the button.
For a button click, these logs represent the order. What is the first log?
console.log("section capture");
console.log("button target");
console.log("section bubble");The first log is section capture, because the outer capture listener runs before the target and before bubbling.
Which method stops the event from reaching later nodes but still lets other listeners on the current node run?
Use stopPropagation(). It prevents the event from reaching more nodes, but same-node listeners still run. Use stopImmediatePropagation() only when those must be skipped too.
A teammate used stopPropagation() to stop a link from navigating. What method should they use instead?
document.addEventListener("click", closeMenu);
menu.addEventListener("click", (event) => {
event.stopPropagation();
});link.addEventListener("click", (event) => {
event.preventDefault();
openInPanel(link.href);
});Use preventDefault() for default browser behavior such as following a link or submitting a form. Propagation controls which listeners hear the event; it does not cancel navigation.
Quiz: check your understanding
7 QUESTIONSTry each question once before reading the explanations.
Question 1 of 7Which phase usually runs first for a click on a button inside a card?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconsole.log("section capture"); console.log("button target"); console.log("section bubble");Choose an answer to see the explanation.
Question 3 of 7You click a
spaninside abutton. In a listener attached to the button, what isevent.currentTarget?Choose an answer to see the explanation.
Question 4 of 7Which event object property is 1 during capture, 2 at target, and 3 during bubble?
Choose an answer to see the explanation.
Question 5 of 7What does
stopPropagation()do?Choose an answer to see the explanation.
Question 6 of 7Which pair gives you bubbling focus events instead of non-bubbling ones?
Choose an answer to see the explanation.
Question 7 of 7Why avoid sprinkling
stopPropagation()everywhere?Choose an answer to see the explanation.
Key takeaways
- Events travel in capture, target, then bubble phases.
event.targetis the original hit;event.currentTargetis the listener’s element.eventPhaseis 1 for capture, 2 at target, and 3 for bubble;composedPath()shows the path.stopPropagation()stops later nodes;stopImmediatePropagation()also skips same-node listeners;preventDefault()stops default actions.- Not every event bubbles. Use bubbling alternatives such as
focusin/focusoutandmouseover/mouseoutwhen an ancestor must hear them.
Remember the one-liner.
Propagation is one event object taking a round trip through the tree: capture in, target, bubble out.
Up next: Event delegation, where you intentionally use bubbling to make one listener handle many elements.