JavaScript Event Bubbling and Capturing
Event bubbling and capturing are two phases in the event propagation model that occurs when events are triggered in the Document Object Model (DOM).
Mastering Event Bubbling and Capturing in JavaScript
When you click a button, the click doesn't only fire on that button — it travels through every ancestor element on the way to and from your target. This travel is called event propagation, and it happens in two directions: capturing (down toward the target) and bubbling (back up to the root). Understanding both is essential for handling events reliably in real applications, especially when nested elements have their own handlers.
This guide explains the propagation model, shows how to listen in each phase, and walks through the practical tools — event.target, event.currentTarget, stopPropagation(), and event delegation — that make these concepts useful day to day. It builds on the basics covered in Introduction to Browser Events and JavaScript Events.
Understanding Event Propagation
Event propagation in the DOM occurs in three phases, in this exact order:
- Capturing phase — the event starts at the top of the tree (
window→document→<html>→ …) and travels down to the target element. - Target phase — the event reaches the element you actually interacted with.
- Bubbling phase — the event travels back up from the target to the root.
│ capturing (down) ▲ bubbling (up)
<html> ▼ │
<div> ▼ │
<p> ───► target (you clicked here)By default, handlers added with addEventListener and inline on* attributes run in the bubbling phase. You opt into the capturing phase explicitly.
Event Bubbling
In the bubbling phase, an event starts at the most specific element (the deepest node you interacted with) and then flows upward through each ancestor toward the document. This is the default behavior for almost every event.
<div onclick="alert('You clicked the DIV!');">
Click me or one of my children:
<p onclick="alert('You clicked the P!');">Click me!</p>
</div>If you click the <p>, you'll see the alert for <p> first, then the alert for <div> as the event bubbles up. If you click the <div> directly (outside the <p>), only the <div> alert fires — the event never reaches <p> because <p> isn't an ancestor of the click point.
Event Capturing
Capturing is the first phase, where the event travels down to the target. It's used far less often than bubbling, but it's handy when you need to intercept an event before any inner handler can run.
To listen during the capturing phase, set the third argument of addEventListener to true (or pass { capture: true }):
<div id="outer">
Click me or one of my children:
<p id="inner">Click me!</p>
</div>
<script>
document.getElementById('outer').addEventListener('click', function () {
alert('Captured on DIV!');
}, true); // true → capturing phase
document.getElementById('inner').addEventListener('click', function () {
alert('Captured on P!');
}, true);
</script>Click the <p> and the alerts fire top-down: DIV first (an ancestor reached on the way down), then P (the target). With bubbling handlers the order would be reversed.
Targeting the Right Element
Inside a handler you usually need to know which element the event started on versus which element the handler is attached to. Two properties answer that:
event.target— the element where the event originated (the deepest one clicked). It stays the same throughout propagation.event.currentTarget— the element whose listener is currently running. It changes as the event moves through the tree, and it equalsthisinside a regular function handler.
function logTargets(event) {
console.log("target:", event.target.tagName);
console.log("currentTarget:", event.currentTarget.tagName);
}
// Imagine this handler is on a <div> and you click a nested <p>:
// target: P (where the click happened)
// currentTarget: DIV (where the listener lives)event.target is what makes event delegation (shown below) possible — one handler on a parent can identify exactly which child was clicked.
Controlling Propagation
JavaScript gives you several methods to control how far an event travels.
| Method | Effect |
|---|---|
event.stopPropagation() | Stops the event from continuing to the next element in the path (no more bubbling/capturing). Handlers on the same element still run. |
event.stopImmediatePropagation() | Stops propagation and prevents any other handlers on the same element from running. |
event.preventDefault() | Cancels the browser's default action (e.g. following a link). Does not stop propagation. |
stopPropagation()andpreventDefault()are independent. Stopping propagation does not cancel default behavior, and vice versa.
A Note on Events That Don't Bubble
Most events bubble, but a few do not — for example focus, blur, mouseenter, mouseleave, and load. For these you cannot rely on a parent handler catching them via bubbling; use the bubbling equivalents (focusin/focusout, mouseover/mouseout) or attach the listener directly. You can always check event.bubbles to confirm whether a given event participates in the bubbling phase.
Practical Examples
Example 1: Stopping Event Bubbling
Sometimes you want a click on a child to not trigger the parent's handler:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Propagation Example</title>
<style>
.container {
width: 200px;
height: 200px;
background-color: lightblue;
padding: 20px;
}
.box {
width: 100px;
height: 100px;
background-color: pink;
margin-top: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container" onclick="alert('You clicked the container!');">
Click the pink box to see event propagation:
<div class="box" onclick="event.stopPropagation(); alert('You clicked the box without bubbling!');"></div>
</div>
</body>
</html>In this example, there's a container with a light blue background containing a pink box. Clicking anywhere inside the container triggers an alert saying "You clicked the container!". However, clicking on the pink box triggers a different alert saying "You clicked the box without bubbling!" because event.stopPropagation() prevents the click event from bubbling up to the container.
Example 2: Using Both Bubbling and Capturing
This example shows how to handle an event in both the capturing and bubbling phases:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Event Capture and Bubbling Example</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
}
#outerContainer {
border: 2px solid #ccc;
padding: 20px;
margin-bottom: 20px;
background-color: #f9f9f9;
border-radius: 10px;
}
#innerElement {
background-color: #ffa8a8;
padding: 10px;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="outerContainer" onclick="alert('Event Bubbled from Outer Container');">
<p style="margin: 0;">Click anywhere in this outer container:</p>
<p id="innerElement">Click me!</p>
</div>
<script>
// Event listener attached to the outer container during the capturing phase
document.getElementById('outerContainer').addEventListener('click', function() {
alert('Event Captured by Outer Container');
}, true);
// Event listener attached to the inner element during the bubbling phase
document.getElementById('innerElement').addEventListener('click', function() {
alert('Event Bubbled from Inner Element');
}, false);
</script>
</body>
</html>When you click the inner element:
- The capturing listener on
#outerContainerruns first, alerting "Event Captured by Outer Container" (it sits on the way down to the target). - The bubbling listener on
#innerElementruns next, alerting "Event Bubbled from Inner Element" (the target itself). - Finally the inline
onclickon#outerContainerruns as the event bubbles up, alerting "Event Bubbled from Outer Container".
This makes the full propagation path visible in order: capture down, hit the target, then bubble back up.
Example 3: Event Delegation
The most common practical use of bubbling is event delegation — attaching a single listener to a parent instead of one listener per child. Because clicks bubble up, the parent can use event.target to figure out which child was clicked. This is efficient and works automatically for elements added later.
const list = document.getElementById("menu");
list.addEventListener("click", function (event) {
// Did the click originate on an <li>?
const item = event.target.closest("li");
if (!item || !list.contains(item)) return;
console.log("You clicked:", item.textContent);
});With this one handler, every current and future <li> inside #menu is covered — you never have to bind (or rebind) listeners on individual items. See Event Handling in the DOM and Dispatching Custom Events for related techniques.
Conclusion
Event propagation moves an event down (capturing) and then up (bubbling) through the DOM. By default handlers run during bubbling; pass true (or { capture: true }) to listen during capturing. Use event.target to find the element that started the event, event.currentTarget for the element handling it, and stopPropagation() / stopImmediatePropagation() to limit how far it travels. Master these and you can build clean, efficient interactions — most notably through event delegation — without scattering listeners across your page.