Event Bubbling and Capturing in JavaScript Explained

Handling events in JavaScript is a crucial part of frontend web development. As a web developer, you should make sure that your application responds efficiently to users interactions. So, your mission would be to create an event handler for each event that you wish your application takes control of. The DOM API provides you with the method addEventListener() that allows you to attach a handler to a specific event in the following way,

anElement.addEventListener('event', handler, options);

Where ‘event‘ represents the event that should be handled, such as a mouse click or keypress or any other type of event. ‘handler‘ is the routine that should be performed when the event occurs. ‘options‘ is an object that specifies some characteristics about the event listener.

You might have used this method a lot while building different sorts of awesome web applications. But, have you ever wonder what happened behind the scenes when an event occurs? Do you know how a target element captures an occurring event?

In fact, in this article, we’ll learn about ‘bubbling and capturing in JavaScript’. It’s an essential element of events handling that every JavaScript developer should know. Understanding it will help you to effectively debug issues that might happen while dealing with events in JavaScript.

The Concept of Event bubbling and Capturing

Event bubbling and capturing are part of the cycle that occurs when an event fires on an element in the DOM.

When an element receives an event, the browser runs three different phases which are, the capturing phase, the target phase and the bubbling phase respectively.

  1. During the capturing phase, the browser notifies the element’s most outer-parent (<html>) of the event, and executes the handler if an event listener has been defined for the event on that element. Then, the browser moves to the next element inside and do the same, then the next one and so on. Untill it reaches the direct parent of the element that has actually recieved the event.
  2. During the target phase, the browser runs the handler if the target element has an event listener registered for this event.
  3. During the bubbling phase, the browser notifies the direct parent of the target element and runs the handler if the parent has an event listener for it. Then, in the same way, the browser does with the next ancestor element, and then the next and so on. Untill it reaches the <html> element.

To sum up, the event propagation happens from up to down to the target – capturing phase -, then from down to up – bubbling phase. Each occurring event goes through these phases.

Now, enough conceptualizing and let’s see that in practice…

A Basic Example

Let’s assume that we have three nested HTML elements in our example here,

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>Bubbling</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" type="text/css" media="screen" href="style.css" />
    <script src="index.js" defer></script>
  </head>
  <body>
    <div id="a">
      A
      <div id="b">
        B
        <div id="c">
            C
        </div>
      </div>
    </div>
  </body>
</html>

You can see that we have three nested elements A>B>C where A is the grandparent element, B is the parent and, C is the child.

Now, just to make it a bit prettier, let’s give it some styling

div{
    border: 1px black solid;
    min-height: 100px;
    min-width: 100px;
    margin: 20px;
}

#a:hover {
 background-color: cadetblue;
}

#b:hover {
    background-color:cornflowerblue;
   }
   
#c:hover {
 background-color: aqua;
}

If you replicate the same code above on your machine, you should get something similar to the following by hovering the mouse over the element C,

Now, let’s move to the piece of code that matters most in this tutorial.

const elemA = document.querySelector("#a");
const elemB = document.querySelector("#b");
const elemC = document.querySelector("#c");

// Event listners
elemA.addEventListener("click", handlerA);
elemB.addEventListener("click", handlerB);
elemC.addEventListener("click", handlerC);

// An array that holds the different phases for events
const phases = ["Event.NONE", "CAPTURING_PHASE", "AT_TARGET", "BUBBLING_PHASE"];

// Handlers
function handlerA(e) {
  console.log(phases[e.eventPhase] + ": Element A clicked!");
}

function handlerB(e) {
  console.log(phases[e.eventPhase] + ":Element B clicked!");
}

function handlerC(e) {
  console.log(phases[e.eventPhase] + ":Element C clicked!");
}

In the first three lines, we use JavaScript to grab the three div elements.

Then, for each element, we register an event listener that will listen for any ‘mouse click‘ event to execute a specific handler.

in line 10, we define a variable phases of constants which represent the different phases of the event flow.

handlerA represents the routine that should be run each time the user clicks on the div element of id=”a”. The same is valid for handlerB, and handlerC for the div element of id=”b”, and the div element of id=”c” respectively.

Notice that in each handler function, we are using the eventPhase property that holds the value of the current phase in which the event flow is being evaluated.

  • 0 for no event is being processed at this time.
  • 1 for the capturing phase
  • 2 for the target phase
  • 3 for the bubbling phase

Now, let’s run some tests…

If we click on the C div (the most inner div), we get the following printed to the console,

AT_TARGET:Element C clicked!
BUBBLING_PHASE:Element B clicked!
iBUBBLING_PHASE: Element A clicked!

Even though we have clicked on the most inner div, we got the handerlB, and the handlerC being executed in the phase of bubbling.

Now, you might ask yourself what about the capturing phase? Where is it gone? Shouldn’t the handlers also run in the capturing phase since any event flow has a capturing phase?

Good question! The answer is because by default all event handlers are registered for the bubbling phase.

In the next section, we’ll see how to use the capturing phase.

You Can Do More…

Use the capturing phase instead

Let’s say that for some reason, you want to take action on A and B before C. In this case, the capturing phase would be a good fit.

Let’s tweak how we define our event listeners as follow,

// Event listners
elemA.addEventListener("click", handlerA, { capture: true });
elemB.addEventListener("click", handlerB, { capture: true });
elemC.addEventListener("click", handlerC);

We are using the third argument in the addEventListener method to decide in which phase should the handler run. In this case, we are assigning true to the capture option for handlerA and handlerB to tell them to run in the capturing phase instead.

Now, if we click on the C div, we get

CAPTURING_PHASE: Element A clicked!
CAPTURING_PHASE:Element B clicked!
AT_TARGET:Element C clicked!

This time, things happened in the reverse order where the capturing phase is used instead.

Stop event propagation

Now, what if you want to disconnect the click event on the C div from affecting the A and B divs? How can we stop event propagation?

All we need to do is using the stopPropagation() method of the event object in the handlerC function,

function handlerC(e) {
  e.stopPropagation();
  console.log(phases[e.eventPhase] + ":Element C clicked!");
}

If we click on the C div, we get

AT_TARGET:Element C clicked!

There’s no effect on B and C divs. All we have is the handlerC being executed in the target phase.

Important: To see the effect of stopping the propagation in this example, you should make sure that the handlerA, and handlerB are in the bubbling mode – the default mode –

Event delegation

One real-life scenario where having a good understanding of bubbling and capturing comes in handy is the application of event delegation in JavaScript.

Event delegation is a shortcut that will allow you to save additional efforts in writing more code. Besides that, it allows using browser’s -machine- resources efficiently. How?

Let’s see the following example to understand how

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>Bubbling</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="index.js" defer></script>
  </head>
  <body>
    <ul id="list">
      <li class="task">Task1</li>
      <li class="task">Task2</li>
      <li class="task">Task3</li>
      <li class="task">Task4</li>
    </ul>
  </body>
</html>

In the example above, we’ve got a list of tasks and we want to make sure that whenever we click on a task it gets ticked by adding the word “Completed!” next to it. How can we achieve that using JavaScript?

One naive solution would be to loop throughout the list and attach an event listener to each task. And define a handler that is going to fire each time the task is clicked.

This solution seems to be perfect for this little amount of tasks…

But wait! What if we had a huge number of tasks on the list? Looping through them and registering an event listener for each one of them would be quite expensive for the browser.

A better solution would be to delegate the action of ticketing the tasks to the parent HTML element list.

Here we go,

const list = document.querySelector("#list");

list.addEventListener("click", function (e) {
  // A check to be sure we're actually clicking on the li element and not elsewhere
  if (e.target.classList.contains("task"))
    e.target.innerText += " - Completed!";
});

This solution took only a few lines of code and fewer resources as we were meant to define only one event listener on the parent element instead of an event listener for each child element.

That’s it for bubbling and capturing in JavaScript! I hope, now, you have a better understanding of it. And I assume that the next time you’ll be using it to your advantage to write better code.

If you have any questions, please, use the comment section below…

One comment

  1. I have a query.
    Let’s assume handler A opens form 1, handler B opens form 2 and handler c opens form 3.
    As per your explanation, if we click C, all the three handlers will run and so all the three forms will open?

Leave a Reply

Your email address will not be published. Required fields are marked *