W3docs

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:

  1. Capturing phase — the event starts at the top of the tree (windowdocument<html> → …) and travels down to the target element.
  2. Target phase — the event reaches the element you actually interacted with.
  3. 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 equals this inside 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.

MethodEffect
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() and preventDefault() 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:

  1. The capturing listener on #outerContainer runs first, alerting "Event Captured by Outer Container" (it sits on the way down to the target).
  2. The bubbling listener on #innerElement runs next, alerting "Event Bubbled from Inner Element" (the target itself).
  3. Finally the inline onclick on #outerContainer runs 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.

Practice

Practice
Which property tells you the element where an event originally occurred, no matter which handler is running?
Which property tells you the element where an event originally occurred, no matter which handler is running?
Practice
By default, in which phase do listeners added with addEventListener run?
By default, in which phase do listeners added with addEventListener run?
Was this page helpful?