• JavaScript

Understanding Event Propagation

Event propagation is a DOM mechanism that triggers the ancestor’s events of the target event. It will have a different execution order depending on the propagation type.

DOM reads your HTML like a hierarchy tree, where each node is an HTML element. In the following code, the anchor has an event that displays an alert saying “Hi!!”.

<div class="node" id="top-node">
  Top Node
  <div class="node" id="mid-node">
    Mid Node
    <a
      class="node"
      id="bottom-node"
      href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      target="_blank"
    >
      Bottom Node
    </a>
  </div>
</div>
const anchor = document.querySelector("#bottom-node");
anchor.addEventListener("click", sayHi);

function sayHi() {
  alert("Hi");
}

Top Node

Mid Node

Bottom Node

The HTML structure above in DOM will look like the following image. You may notice that every element is nested to each other, so to reach the anchor, you need to pass through every anchor’s parent.

The browser can go ahead with 1 of 2 phases:

  • Capturing Phase - the browser searches for events from the window and dives down to reach the event target.
  • Bubbling Phase - the browser starts on the event target and Bubbles up to the window.
  • There is the Target Phase, this always happens when an event is executed.

Capturing Phase

The browser builds a path from the window to the event target, then goes through every node. Each node is analyzed, and there will be some steps to do before moving forward:

Example

You are going to see event propagation with the capturing propagation. The structure will be the same as above, but some an URL to open a new page will be added to see how default events work in propagation.

<div class="node" id="top-node">
  Top Node
  <div class="node" id="mid-node">
    Mid Node
    <a
      class="node"
      id="bottom-node"
      href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      target="_blank"
    >
      Bottom Node
    </a>
  </div>
</div>
let nodes = document.querySelectorAll(".node");
nodes = [...nodes];

// Add an event to the 3 elements to know its current phase
nodes.map((node) => {
  // Enabling Capturing Propagation - Disabling Bubbling Propagation
  node.addEventListener("click", showPhase, true);
});

// Add main event
const anchor = document.querySelector("#bottom-node");
anchor.addEventListener("click", sayHi);

function sayHi() {
  alert("Hi");
}

function showPhase(event) {
  const phase = event.eventPhase;
  let phaseName;
  // Validate phase type
  if (phase === 1) {
    phaseName = "Capturing";
  } else if (phase === 2) {
    phaseName = "Target";
  } else if (phase === 3) {
    phaseName = "Bubbling";
  }
  alert(`${this.id}: ${phaseName} Phase`);
}

By default, the addEventListener parameter useCapture is false. To enable the capturing propagation and disable the bubbling one, just add the parameter true within the addEventLister after the listener. The event.eventPhase() will return 3 different values:

  1. Capturing Phase.
  2. Target Phase.
  3. Bubbling Phase.

Click one of the elements to see how Capturing Propagation works:

Top Node

Mid Node

Bottom Node

After the main event is executed, the default event occurs.

Target Phase

Once every parent has been checked, the event target occurs. This event has to be declared with the onClick attribute or with the addEventListener() method.

This phase will always occur if there is no event.stopPropagation() or event.stopImmediatePropagation() to block it. During the Capturing Propagation, it will execute after the parent’s events, and during the Bubbling Propagation, it will execute before them. And always will be executed before the default event.

Bubbling Phase

When the event target executes its event, the browser goes all the way back to the path, checking each node with the same steps as the capturing one:

  1. Check if there is an event or events. If yes, then execute it.
  2. Check if the node called an event.stopPropagation() or event.stopImmediatePropagation(). If it was, stop the process, event target, or other nested events to not execute. If not, go to the next node.

Looking at the structure, you can notice if you want to reach <a>, you need to go through each parent, including the window and document.

Example

In the Bubbling Propagation, the event execution order goes backward. It starts with the target event and goes go towards the window. As the Bubbling Propagation is set by default, you don’t need to send an argument for the useCapture parameter, or you can set it to false. It is on you how to.

<div class="node" id="top-node">
  Top Node
  <div class="node" id="mid-node">
    Mid Node
    <a
      class="node"
      id="bottom-node"
      href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      target="_blank"
    >
      Bottom Node
    </a>
  </div>
</div>
let nodes = document.querySelectorAll(".node");
nodes = [...nodes];

// Add an event to the 3 elements to know its current phase
nodes.map((node) => {
  // Enabling Bubbling Propagation - Disable Capturing Propagation
  node.addEventListener("click", showPhase, false);
});

function showPhase(event) {
  const phase = event.eventPhase;
  let phaseName;
  // Validate phase type
  if (phase === 1) {
    phaseName = "Capturing";
  } else if (phase === 2) {
    phaseName = "Target";
  } else if (phase === 3) {
    phaseName = "Bubbling";
  }
  alert(`${this.id}: ${phaseName} Phase`);
}

// Add main event
const anchor = document.querySelector("#bottom-node");
anchor.addEventListener("click", sayHi);

function sayHi() {
  alert("Hi");
}

Click one of the elements to see how it works:

Top Node

Mid Node

Bottom Node

Once the Bubbling Propagation ends, the default event of the event target will be triggered.

Stop Event Propagation

stopPropagation(), stopImmediatePropagation() to stop a propagation you have to use one of these methods. The difference is that stopPropagation() stops the hierarchy propagation. It cancels any next node event (parents for bubbling and children for capturing). In the following examples, you can see how to stop propagation in both types of propagation. Example 1 Using the stopPropagation() to stop a Capturing Propagation. The method is implemented on the mid-node.

Example 1

Using stopPropagation() to stop a Capturing Propagation. The method is implemented on the mid-node.

<div class="node" id="top-node">
  Top Node
  <div class="node" id="mid-node">
    Mid Node
    <a
      class="node"
      id="bottom-node"
      href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      target="_blank"
    >
      Bottom Node
    </a>
  </div>
</div>
let nodes = document.querySelectorAll(".node");
nodes = [...nodes];
const div = document.querySelector("#mid-node");
div.addEventListener("click", stopEvent, true);

function stopEvent(event) {
  // Stop Propagation
  event.stopPropagation();
  alert("Here ends");
}

// Add an event to the 3 elements to know its current phase
nodes.map((node) => {
  // Enabling Bubbling Propagation - Disable Capturing Propagation
  node.addEventListener("click", showPhase, true);
});

function showPhase(event) {
  const phase = event.eventPhase;
  let phaseName;

  // Validate phase type
  if (phase === 1) {
    phaseName = "Capturing";
  } else if (phase === 2) {
    phaseName = "Target";
  } else if (phase === 3) {
    phaseName = "Bubbling";
  }
  alert(`${this.id}: ${phaseName} Phase`);
}

function sayHi() {
  alert("Hi");
}

// Add main event
const anchor = document.querySelector("#bottom-node");
anchor.addEventListener("click", sayHi);

Click bottom-node to see what happens:

Top Node

Mid Node

Bottom Node

The alert for the bottom-node that is the event target is not executed because of the stopPropagation(). The showPhase() alert for mid-node is still displayed.

The anchor URL was deleted to avoid continuous emerging rick rolls.

Example 2

Using stopPropagation() to stop a Bubbling Propagation. The method is implemented on mid-node.

<div class="node" id="top-node">
  Top Node
  <div class="node" id="mid-node">
    Mid Node
    <a
      class="node"
      id="bottom-node"
      href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      target="_blank"
    >
      Bottom Node
    </a>
  </div>
</div>
let nodes = document.querySelectorAll(".node");
nodes = [...nodes];

const div = document.querySelector("#mid-node");
div.addEventListener("click", stopEvent, false);

function stopEvent(event) {
  // Stop Propagation
  event.stopPropagation();
  alert("Here ends");
}

// Add an event to the 3 elements to know its current phase
nodes.map((node) => {
  // Enabling Bubbling Propagation - Disable Capturing Propagation
  node.addEventListener("click", showPhase, false);
});

function showPhase(event) {
  const phase = event.eventPhase;
  let phaseName;

  // Validate phase type
  if (phase === 1) {
    phaseName = "Capturing";
  } else if (phase === 2) {
    phaseName = "Target";
  } else if (phase === 3) {
    phaseName = "Bubbling";
  }
  alert(`${this.id}: ${phaseName} Phase`);
}

function sayHi() {
  alert("Hi");
}

// Add main event
const anchor = document.querySelector("#bottom-node");
anchor.addEventListener("click", sayHi);

Click bottom-node to see what happens:

Top Node

Mid Node

Bottom Node

The event bubbles until the mid-node events are executed

The section alert does not occur because of the stopPropagation(). The showPhase() alert for the <div> is still displayed.

Example 3

Using the stopImmediatePropagation() to stop a Capturing Propagation. The method is implemented on the mid-node.

<div class="node" id="top-node">
  Top Node
  <div class="node" id="mid-node">
    Mid Node
    <a
      class="node"
      id="bottom-node"
      href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      target="_blank"
    >
      Bottom Node
    </a>
  </div>
</div>
let nodes = document.querySelectorAll(".node");
nodes = [...nodes];

const div = document.querySelector("#mid-node");
div.addEventListener("click", stopEvent, true);

function stopEvent(event) {
  // Stop Propagation
  event.stopImmediatePropagation();
  alert("Here ends");
}

// Add an event to the 3 elements to know its current phase
nodes.map((node) => {
  // Enabling Bubbling Propagation - Disable Capturing Propagation
  node.addEventListener("click", showPhase, true);
});

function showPhase(event) {
  const phase = event.eventPhase;
  let phaseName;

  // Validate phase type
  if (phase === 1) {
    phaseName = "Capturing";
  } else if (phase === 2) {
    phaseName = "Target";
  } else if (phase === 3) {
    phaseName = "Bubbling";
  }
  alert(`${this.id}: ${phaseName} Phase`);
}

// Add main event
const anchor = document.querySelector("#bottom-node");
anchor.addEventListener("click", sayHi);

function sayHi() {
  alert("Hi");
}

Click bottom-node to see what happens:

Top Node

Mid Node

Bottom Node

The event drills down until the mid-node event is executed, the bottom-node alert does not occur.

The showPhase() alert of the mid-node is not displayed anymore because of the stopImmediatePropagation().

Example 4

Using the stopImmediatePropagation() to stop a Bubbling Propagation. The method is implemented on the mid-node.

<div class="node" id="top-node">
  Top Node
  <div class="node" id="mid-node">
    Mid Node
    <a
      class="node"
      id="bottom-node"
      href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      target="_blank"
    >
      Bottom Node
    </a>
  </div>
</div>
let nodes = document.querySelectorAll(".node");
nodes = [...nodes];

const div = document.querySelector("#mid-node");
div.addEventListener("click", stopEvent, false);

function stopEvent(event) {
  // Stop Propagation
  event.stopImmediatePropagation();
  alert("Here ends");
}

// Add an event to the 3 elements to know its current phase
nodes.map((node) => {
  // Enabling Bubbling Propagation - Disable Capturing Propagation
  node.addEventListener("click", showPhase, false);
});

function showPhase(event) {
  const phase = event.eventPhase;
  let phaseName;

  // Validate phase type
  if (phase === 1) {
    phaseName = "Capturing";
  } else if (phase === 2) {
    phaseName = "Target";
  } else if (phase === 3) {
    phaseName = "Bubbling";
  }
  alert(`${this.id}: ${phaseName} Phase`);
}

function sayHi() {
  alert("Hi");
}
// Add main event
const anchor = document.querySelector("#bottom-node");
anchor.addEventListener("click", sayHi);

Click bottom-node to see what happens:

Top Node

Mid Node

Bottom Node

The event Bubbles until the mid-node events are executed, and the top-node alert does not occur.

The showPhase() of mid-node is not displayed anymore.

Use Cases

Maybe you are wondering when to use all of this. To be honest, there are not as many use cases for these concepts as are for Event Propagation, etc. But, your tool kit to create solutions has won a great tool. To apply propagation and how to stop it. You will see 2 use cases.

The following use cases can have a better solution without too much code and good practices, but we solve them with Event Propagation for the sake of the article.

Watch List

In this use case, we got a list item that contains a <span> and a <button>. We added an event to open the series trailer once the list item is clicked. Also, a click event was added to the button to check the series you watched.

<div>
  <h1>Watch List</h1>
  <ul class="list">
    <li>
      <span>Game of Thrones</span>
      <button type="submit">Check</button>
    </li>
    <li>
      <span>Daredevil</span>
      <button type="submit">Check</button>
    </li>
    <li>
      <span>Brooklyn 99</span>
      <button type="submit">Check</button>
    </li>
    <li>
      <span>Breaking Bad</span>
      <button type="submit">Check</button>
    </li>
  </ul>
</div>
const items = [...document.querySelectorAll("li")];

// Add event to every list item
items.map((item) => {
  item.addEventListener("click", seeTrailer);
});

function seeTrailer() {
  // Open trailer
  window.open(
    "https://www.youtube.com/watch?v=KPLWWIOCOOQ",
    "",
    "width=600,height=340"
  );
}

function checkItem(event) {
  // Open trailer
  const item = this.parentElement;
  item.classList.toggle("list--check");
}

const buttons = [...document.querySelectorAll("button")];
// Add event to every button
buttons.map((button) => {
  button.addEventListener("click", checkItem);
});

Click any place inside the list item to open the trailer and try to check it:

Watch List
  • Game of Thrones
  • Daredevil
  • Brooklyn 99
  • Breaking Bad

To solve it we just need to add the stopPropagation() method.

<div>
  <h1>Watch List</h1>
  <ul class="list">
    <li>
      <span>Game of Thrones</span>
      <button type="submit">Check</button>
    </li>
    <li>
      <span>Daredevil</span>
      <button type="submit">Check</button>
    </li>
    <li>
      <span>Brooklyn 99</span>
      <button type="submit">Check</button>
    </li>
    <li>
      <span>Breaking Bad</span>
      <button type="submit">Check</button>
    </li>
  </ul>
</div>
const items = [...document.querySelectorAll("li")];

// Add event to every list item
items.map((item) => {
  item.addEventListener("click", seeTrailer);
});

function seeTrailer() {
  // Open trailer
  window.open(
    "https://www.youtube.com/watch?v=KPLWWIOCOOQ",
    "",
    "width=600,height=340"
  );
}

function checkItem(event) {
  event.stopPropagation(); // <-- Solution // Open trailer
  const item = this.parentElement;
  item.classList.toggle("list--check");
}

const buttons = [...document.querySelectorAll("button")];
// Add event to every button
buttons.map((button) => {
  button.addEventListener("click", checkItem);
});

Click any place inside the list item to open the trailer and check a series:

Watch List
  • Game of Thrones
  • Daredevil
  • Brooklyn 99
  • Breaking Bad

We can add a fancy modal that only disappears if the background is clicked. First, we add an event to the button to toggle the modal, and then we add the same event to the modal container to toggle it.

<div>
  <h1>Click the button to open the modal</h1>
  <button type="button">Modal</button>
  <div>
    <p>Touch any place out of the modal to close it</p>
  </div>
</div>
const button = document.querySelector("button");
const modalContainer = document.querySelector("div");
// Show modal
button.addEventListener("click", toggleModal);

function toggleModal() {
  modalContainer.classList.toggle("active-div");
}
// Hide modal
modalContainer.addEventListener("click", toggleModal);

Click the button and try click whitin the modal to see if it stays open:

Wait a minute. This works even if you click within the modal. This is not the behavior we want. To fix it, we can add an event to the modal to stop the Bubbling Propagation.

<div>
  <h1>Click the button to open the modal</h1>
  <button type="button">Modal</button>
  <div>
    <p>Touch any place out of the modal to close it</p>
  </div>
</div>
const button = document.querySelector("button");
const modalContainer = document.querySelector("div");
const modal = document.querySelector(".modal");

// Show modal
button.addEventListener("click", toggleModal);

function toggleModal() {
  modalContainer.classList.toggle("active-div");
}
// Hide modal
modalContainer.addEventListener("click", toggleModal);
modal.addEventListener("click", (event) => event.stopPropagation()); //<--Solution

Click the button and click out the modal to close it:

It is Done! You learned how DOM manages events with propagation, building a path to the event target, looking for events or stop event methods in 2 different phases, top to bottom for Capturing and upside down for Bubbling Propagation. You can now stop propagation and create divergent behaviors and effects in your web apps.