JavaScript Event Bubbling and Capturing Guide

Understand event bubbling and capturing in JavaScript. Learn how events travel through the DOM, the difference between the three phases, and when to use each.

5 min read

When you click a button inside a <div> inside a <section>, the click event does not just fire on the button. It travels through every ancestor element in a predictable order. This journey is called event propagation, and it has three phases: capture, target, and bubble.

The three phases of event propagation

The diagram shows a click on a <button>. First the event travels down from document through every ancestor to the button (capture phase). Then it fires on the button itself (target phase). Then it travels back up through the same ancestors (bubble phase).

By default, addEventListener registers listeners for the bubble phase. Adding { capture: true } registers them for the capture phase instead.

The Bubble Phase: Default Behavior

Bubbling is the default mode. When you click a child element, the event fires on the child first, then on its parent, then on the grandparent, all the way up to document:

javascriptjavascript
document.querySelector("#child").addEventListener("click", () => {
  console.log("Child clicked");
});
document.querySelector("#parent").addEventListener("click", () => {
  console.log("Parent clicked");
});
document.querySelector("#grandparent").addEventListener("click", () => {
  console.log("Grandparent clicked");
});

Click the child element, and all three listeners fire even though only the child was clicked directly. The console shows them in order from deepest to shallowest:

texttext
Child clicked
Parent clicked
Grandparent clicked

The event starts at the deepest element and bubbles up. This is why clicking a <span> inside a <button> still triggers the button's click handler.

Most events bubble: click, keydown, change, submit, mouseover, and many others. Notable exceptions are focus, blur, and element-level scroll. Use bubbling to your advantage with /javascript/javascript-event-delegation-complete-tutorial.

The Capture Phase: Listening Early

The capture phase travels in the opposite direction: from the root down to the target. Listeners in this phase fire before bubble-phase listeners:

javascriptjavascript
grandparent.addEventListener("click", () => {
  console.log("Grandparent CAPTURE");
}, { capture: true });
parent.addEventListener("click", () => {
  console.log("Parent CAPTURE");
}, { capture: true });
 
grandparent.addEventListener("click", () => console.log("Grandparent bubble"));
parent.addEventListener("click", () => console.log("Parent bubble"));
child.addEventListener("click", () => console.log("Child bubble"));

Clicking the child now produces a different order than the bubble-only example, because the capture listeners run first on their way down before anything on the target fires:

texttext
Grandparent CAPTURE
Parent CAPTURE
Child bubble
Parent bubble
Grandparent bubble

Capture-phase listeners fire top-down. Then target-phase listeners fire (the child's bubble listener, since it is on the target). Then bubble-phase listeners fire bottom-up.

Stopping Propagation

event.stopPropagation() prevents the event from traveling further:

javascriptjavascript
child.addEventListener("click", (event) => {
  event.stopPropagation();
  console.log("Child clicked. Propagation stopped.");
});
 
parent.addEventListener("click", () => {
  console.log("This will NOT fire.");
});

After the child's handler calls stopPropagation(), the parent never receives the event. This is useful when a child element's click should be handled independently from its parent's click behavior.

stopPropagation only stops the event from traveling to other elements. It does not stop other listeners on the same element from firing. For that, use event.stopImmediatePropagation():

javascriptjavascript
child.addEventListener("click", (event) => {
  event.stopImmediatePropagation();
  console.log("First handler. No other handlers on child will fire.");
});
 
child.addEventListener("click", () => {
  console.log("This will NOT fire.");
});

Bubbling vs Capturing: When to Use Each

Bubble (default)Capture
DirectionTarget → ancestors (up)Root → target (down)
Use caseEvent delegation, most handlersIntercepting events before children get them
How to enableOmit capture or set { capture: false }Set { capture: true }
Fires relative to targetAfter target handlersBefore target handlers

Use bubble for almost everything. It is the default, it works with event delegation, and it matches the mental model of "child handles it first, then parent."

Use capture when you need to intercept an event before any child element has a chance to handle it. For example, a drag-and-drop system might use capture to claim a mousedown on the container before any draggable child processes it.

Practical Example: Modal Click-Outside Detection

A common pattern is closing a modal when the user clicks outside it. This uses bubbling:

javascriptjavascript
const modal = document.querySelector("#modal");
const overlay = document.querySelector("#overlay");
 
overlay.addEventListener("click", (event) => {
  // Close only if the overlay itself was clicked, not the modal inside
  if (event.target === overlay) {
    overlay.style.display = "none";
  }
});

When the user clicks the modal content, event.target is an element inside the modal, so the overlay stays open. When the user clicks the dark overlay area (outside the modal), event.target is the overlay itself, and it closes.

Non-Bubbling Events and Their Alternatives

Non-bubbling eventBubbling alternative
focusfocusin
blurfocusout
scroll (on elements)None; listen on document instead
load (on images)Use error/load on a parent with capture

For focus and blur, the bubbling alternatives focusin and focusout work identically but propagate through the DOM, which means they support event delegation.

Common Mistakes

Confusing stopPropagation with preventDefault. stopPropagation stops the event from traveling to parent elements. preventDefault blocks the browser's built-in behavior for the event. They solve completely different problems. For more on preventDefault, see /javascript/using-preventdefault-in-javascript-full-guide.

Expecting focus and blur to delegate. These events do not bubble. If you need to listen for focus events on dynamically added inputs, either use focusin/focusout or attach listeners directly to each input.

Using capture for everything. Capture is a specialized tool. Using it everywhere makes event flow harder to reason about because handlers fire in the opposite order of what most developers expect. Default to bubble mode unless you have a specific reason for capturing.

Rune AI

Rune AI

Key Insights

  • Events travel in three phases: capture, target, and bubble.
  • Bubbling goes up from the target to ancestors (the default listener mode).
  • Capturing goes down from the root to the target (use { capture: true }).
  • Use stopPropagation to prevent events from traveling further.
  • Event delegation relies on bubbling to work.
  • Focus and blur do not bubble; use focusin and focusout instead.
RunePowered by Rune AI

Frequently Asked Questions

Does stopPropagation also prevent the default action?

No. stopPropagation only stops the event from traveling to parent elements. It does not block the browser's default action. Use preventDefault to block the default action, and call both if you need both behaviors.

Do all events bubble?

No. Most user interaction events like click, keydown, and change bubble. Events like focus, blur, and scroll on elements do not bubble. Use focusin and focusout as bubbling alternatives to focus and blur.

Can I stop an event during the capture phase?

Yes. Calling stopPropagation inside a capture-phase listener stops the event from reaching the target and any bubble-phase listeners. The capture-phase listeners on the same element and subsequent ancestors still run.

Conclusion

Event bubbling and capturing are the two mechanisms that control how events travel through the DOM tree. Bubbling goes from the target up to the root. Capturing goes from the root down to the target. Understanding both is essential for event delegation, controlling event flow, and debugging why a handler fires when you did not expect it.