JavaScript DOM: Event Propagation

JavaScript DOM: Event Propagation

JavaScript events flow through the DOM (Document Object Model) in a process called event propagation. When you interact with elements on a web page, like clicking a button inside a box, the event doesn’t just affect that button alone. Instead, it moves through a sequence of phases in the DOM tree, traveling from the top (like the whole document) down to the target element, and then back up again. This process allows different elements to respond to the same event in order.

Event propagation has three main phases: capturing, target, and bubbling. Understanding these phases helps you control exactly how your event handlers react, and in what order. You can decide if an event should be caught early during the capture phase, or only after it reaches the target and bubbles up. This article will take you through how to use event propagation step-by-step with fun, simple examples that you can run in any browser.

Basic Event Propagation Example

To begin, let’s see how event propagation works by default, focusing on the bubbling phase. Bubbling means the event starts at the target element where the action happened, then travels up through its parents.

Consider a gray outer box containing a blue inner button. We attach click event listeners to both elements. When you click the button, the button’s listener runs first, then the event bubbles up and triggers the outer box’s listener.

<!DOCTYPE html>
<html>
<head>

  <title>Event Bubbling Example</title>

  <style>

    #box {
      padding: 20px;
      background-color: lightblue;
      width: 200px;
      text-align: center;
    }

  </style>

</head>
<body>

<div id="box">
  Parent Box
  <button id="btn">Click Me</button>
</div>

<script>

  const box = document.getElementById('box');
  const btn = document.getElementById('btn');

  box.addEventListener('click', () => {
    alert('Box clicked!');
  });

  btn.addEventListener('click', () => {
    alert('Button clicked!');
  });

</script>

</body>
</html>

In this example, clicking the button first shows “Button clicked!” Then, because the event bubbles, it triggers the outer box listener, which shows “Outer box clicked!” This happens because after the event reaches the target (the button), it travels upward through the DOM tree, hitting parent elements’ listeners on the way.

Using Event Capturing Phase

By default, event listeners listen during the bubbling phase. But you can also listen during the capturing phase, which happens before the event reaches the target. Capturing starts from the top element and moves down toward the target.

You can enable capturing by passing a third argument (true) or an options object with { capture: true } to addEventListener. Let’s see this with the same outer box and button, but now the outer box listens during capturing:

<!DOCTYPE html>
<html>
<head>

  <title>Event Capturing Example</title>

  <style>

    #container {
      padding: 20px;
      background-color: lightgreen;
      width: 200px;
      text-align: center;
    }

  </style>

</head>
<body>

<div id="container">
  Container
  <button id="innerBtn">Press Me</button>
</div>

<script>

  const container = document.getElementById('container');
  const innerBtn = document.getElementById('innerBtn');

  container.addEventListener('click', () => {
    alert('Container clicked during capturing!');
  }, true); // true sets capturing phase

  innerBtn.addEventListener('click', () => {
    alert('Button clicked!');
  });

</script>

</body>
</html>

Here, clicking the button triggers the outer box alert first, because capturing runs from the top down before the event reaches the button itself. Then the button’s own listener runs. This shows the difference between capturing and bubbling: capturing happens earlier, moving down the DOM, while bubbling happens later, moving up.

Stopping Propagation

Sometimes you want to stop the event from traveling further in the DOM. You can do this by calling event.stopPropagation() inside an event handler. This prevents the event from moving to other listeners in the propagation chain.

Let’s update the button listener from before to stop propagation when clicked:

<!DOCTYPE html>
<html>
<head>

  <title>Stop Propagation Example</title>

  <style>

    #frame {
      padding: 20px;
      background-color: peachpuff;
      width: 220px;
      text-align: center;
    }

  </style>

</head>
<body>

<div id="frame">
  Frame
  <button id="stopBtn">No Bubble</button>
</div>

<script>

  const frame = document.getElementById('frame');
  const stopBtn = document.getElementById('stopBtn');

  frame.addEventListener('click', () => {
    alert('Frame clicked!');
  });

  stopBtn.addEventListener('click', (event) => {

    alert('Button clicked!');
    event.stopPropagation(); // Prevents bubbling to frame

  });

</script>

</body>
</html>

With this code, when you click the button, only the button’s alert shows. The event stops at the button and does not bubble up to trigger the outer box’s listener. This is useful when you want to isolate certain interactions and prevent them from affecting parent elements.

Stopping Immediate Propagation

If an element has multiple listeners for the same event, you might want to stop all other listeners from running after one fires. For that, use event.stopImmediatePropagation(). This not only stops the event from propagating but also stops any other listeners on the same element.

Here’s an example with two listeners on a button. The first listener stops immediate propagation, so the second listener never runs:

<!DOCTYPE html>
<html>
<head>

  <title>Stop Immediate Propagation</title>

  <style>

    button {
      padding: 10px 20px;
      margin: 20px;
      font-size: 16px;
    }

  </style>

</head>
<body>

<button id="myBtn">Click Me</button>

<script>

  const btn = document.getElementById('myBtn');

  btn.addEventListener('click', (event) => {
    alert('First listener');
    event.stopImmediatePropagation();
  });

  btn.addEventListener('click', () => {
    alert('Second listener');
  });

</script>

</body>
</html>

When you click this button, only “First listener” appears. The second listener is skipped because the first called stopImmediatePropagation(). This method gives you fine control over which handlers run on the same element.

Combining Capturing, Target, and Bubbling Listeners

You can add multiple listeners for the same event on the same element but listening at different phases — capturing and bubbling.

Let’s add two listeners to the button: one for capturing and one for bubbling:

<!DOCTYPE html>
<html>
<head>

  <title>Capturing and Bubbling Listeners</title>

  <style>

    #myBtn {
      padding: 10px 20px;
      font-size: 16px;
      margin: 20px;
      cursor: pointer;
    }

  </style>

</head>
<body>

<button id="myBtn">Click Me</button>

<script>

  const btn = document.getElementById('myBtn');

  btn.addEventListener('click', () => {
    alert('Bubbling listener');
  }); // Default is bubbling phase

  btn.addEventListener('click', () => {
    alert('Capturing listener');
  }, true); // Capturing phase

</script>

</body>
</html>

Clicking the button first triggers the “Capturing listener” alert because capturing runs first, then the “Bubbling listener” alert runs after the event reaches the target and bubbles back up. This demonstrates how the same event fires in different phases on the same element.

Removing Event Listeners Related to Propagation

You can remove event listeners with removeEventListener() but you must specify the same function and phase used when adding them.

Here’s an example where a capturing listener is added and then removed after one click:

<!DOCTYPE html>
<html>
<head>

  <title>Remove Capturing Listener Example</title>

  <style>

    #myBtn {
      padding: 10px 20px;
      font-size: 16px;
      margin: 20px;
      cursor: pointer;
    }

  </style>

</head>
<body>

<button id="myBtn">Click Me</button>

<script>

  const btn = document.getElementById('myBtn');

  function onClickCapture() {
    alert('Captured click!');
  }

  btn.addEventListener('click', onClickCapture, true); // Capturing phase listener

  btn.addEventListener('click', () => {

    alert('Bubbling click!');
    btn.removeEventListener('click', onClickCapture, true); // Remove capturing listener after first click

  });

</script>

</body>
</html>

The first click shows both alerts. After that, the capturing listener is removed, so future clicks only show the bubbling alert. When removing listeners, you must match the exact parameters (function and capture flag) used when adding them.

Practical Example: Nested Menus with Propagation Control

To see propagation in action, let’s create a simple nested menu. Clicking on a submenu item triggers its action, but clicking outside closes the entire menu. We’ll use event propagation to detect where clicks happen.

<!DOCTYPE html>
<html>
<head>

  <title>Nested Menu Example</title>

  <style>

    #menu {
      padding: 10px;
      background: #ddd;
      width: 200px;
      margin: 20px;
      border: 1px solid #aaa;
    }

    #submenu {
      display: none;
      padding-left: 20px;
      margin-top: 10px;
      background: #eee;
      list-style-type: disc;
    }

    #submenu li {
      cursor: pointer;
      padding: 5px 0;
    }

    #submenu li:hover {
      background: #ccc;
    }

  </style>

</head>
<body>

<div id="menu">

  <button id="menuBtn">Menu</button>

  <ul id="submenu">
    <li>Option 1</li>
    <li>Option 2</li>
    <li>Option 3</li>
  </ul>

</div>

<script>

  const menuBtn = document.getElementById('menuBtn');
  const submenu = document.getElementById('submenu');
  const menu = document.getElementById('menu');

  menuBtn.addEventListener('click', (event) => {

    submenu.style.display = submenu.style.display === 'block' ? 'none' : 'block';
    event.stopPropagation(); // Prevent immediate close

  });

  submenu.addEventListener('click', (event) => {

    alert(`You selected: ${event.target.textContent}`);
    event.stopPropagation(); // Prevent close on submenu click

  });

  document.addEventListener('click', () => {
    submenu.style.display = 'none';
  });

</script>

</body>
</html>

Clicking the menu button toggles the submenu open or closed. Clicking an option shows an alert with the choice. Clicking outside closes the submenu. We use stopPropagation() to stop clicks inside the menu from bubbling to the document, which would close the submenu immediately.

Conclusion

Event propagation is how events move through the DOM in three phases: capturing, target, and bubbling. By listening during capturing or bubbling, and by using stopPropagation() or stopImmediatePropagation(), you can control exactly which handlers run and when. You can add and remove listeners for different phases and combine them on the same element.

By learning how propagation works, you can handle complex interactions in your JavaScript applications with clear, manageable code.

References

If you want to learn more about event propagation and JavaScript event handling, these links are very helpful:

Scroll to Top