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.
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 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:
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:
Child clicked
Parent clicked
Grandparent clickedThe 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:
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:
Grandparent CAPTURE
Parent CAPTURE
Child bubble
Parent bubble
Grandparent bubbleCapture-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:
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():
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 | |
|---|---|---|
| Direction | Target → ancestors (up) | Root → target (down) |
| Use case | Event delegation, most handlers | Intercepting events before children get them |
| How to enable | Omit capture or set { capture: false } | Set { capture: true } |
| Fires relative to target | After target handlers | Before 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:
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 event | Bubbling alternative |
|---|---|
focus | focusin |
blur | focusout |
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
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.
Frequently Asked Questions
Does stopPropagation also prevent the default action?
Do all events bubble?
Can I stop an event during the capture phase?
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.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.