JavaScript Event Bubbling and Capturing

Among the most used terminology in JavaScript at the time of event flow are bubbling and capturing. In general, the event flow process is completed by the following three concepts: event capturing, event target, and event bubbling.

Before starting to explain the concept of bubbling, let’s consider a case.

In the example below, the handler is assigned to <div>, but will run whenever there is a click on any nested tag, such as <em> or <code>:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body>
    <div onclick="alert('Welcome to W3Docs')">
      <em>Click on <code>EM</code>, the handler on <code>DIV</code> starts.</em>
    </div>
  </body>
</html>

At first sight, it seems quite strange, why the handler on <div> runs, when the click was on <em>. You will understand that better after learning what bubbling is.

Bubbling

The principle of bubbling is simple. Whenever an event occurs on an element, at first place it will run the handler on it, then its parent, then on other ancestors. Imagine that you have 3 nested elements FORM > DIV > P, and each of them has a handler, like this:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the Document</title>
    <style>
      body * {
        margin: 20px;
        border: 1px solid green;
      }
    </style>
  </head>
  <body>
    <form onclick="alert('form')">
      FORM
      <div onclick="alert('div')">
        DIV
        <p onclick="alert('p')">P</p>
      </div>
    </form>
  </body>
</html>

So, if you click the inner <p>, onclick will run first, like here:

  1. Firstly, on the <p>.
  2. Secondly, on the outer <div>.
  3. Thirdly, on the outer <form>.
  4. Then upwards until the document object.

So, at the time you click on <p>, there are 3 alerts. such as p → div → form. The process above is known as bubbling, as events “bubble” from the inner element through the parents just as bubbles in the water.

event.target

The parent element’s handler can always receive the details where it happened. The most deeply nested element causing the event, is known as a target element, and it is accessible as event.target.

Now, let’s see the differences in this (=event.currentTarget):

  • the event.target is the target element, initiating the event. It will not change through the process of bubbling.
  • this is considered the “current” element, which has a handler that is currently running on it.

For example, if you have a single handler form.onclick, it will catch the overall clicks within the form. It doesn’t even matter where the click occurred, it will bubble up to <form> and run the handler.

In the handler form.onclick, this (=event.currentTarget) is considered the <form> element, as the handler runs on it. On its turn, the event.target is considered the actual element within the form, which was clicked.

Stopping Bubbling

As a rule, the bubbling goes upwards up to the <html> , then to the document object. Some events can even get to the window and call handlers on the way. By the way, any handler can decide to stop bubbling when the event has been fully processed. For that purpose, you can use the event.stopPropagation() method.

Let’s look through an example where if you click on the <<button>, body.onclick will not work:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body onclick="alert(`the bubbling doesn't reach here`)">
    <button onclick="event.stopPropagation()">Click me</button>
  </body>
</html>

Please note that you shouldn’t stop bubbling without a real need.

Bubbling is a rather convenient process. Don’t stop it without a reason. At times event.stopPropagation() generates hidden pitfalls that may cause problems later.

As usual, there is no need to prevent bubbling. Your task can be carried out in other ways, too. One of them is to write the data into the event object in one handler, reading it in another one.

Capturing

Another phase of event processing is known as capturing. In practice, it is rarely used but can be useful sometimes. With capturing, firstly, the outermost element captures the event. Then the event is propagated to the inner elements.

So, for reaching the click on <tr> , the event should pass through the chain of ancestors down to the exact element. This process is called “capturing”. Then, it gets to the target, triggering there ( it is the target phase), and only after that, it goes up (the phase of bubbling), calling the handlers on its path.

In this chapter, we paid more attention to bubbling, as capturing is not used often and is normally invisible to us.

For catching an event on the capturing phase, setting the handler capture to true is a necessity, as shown in this example:

elem.addEventListener(..., {
  capture: true
})
// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)

The capture option has two possible values:

  • when it’s false, it means that the handler is set on the bubbling phase.
  • when it’s true- the handler is set on the capturing phase.

Also, take into account that, although 3 phases are normally distinguished, the target phase is never handled separately.

The action of both bubbling and capturing is illustrated in the example below:

<!DOCTYPE html>
<html>
  <head>
    <title>Title of the Document</title>
    <style>
      body * {
        margin: 20px;
        border: 1px solid green;
      }
    </style>
  </head>
  <body>
    <form>
      FORM
      <div>
        DIV
        <p>P</p>
      </div>
    </form>
    <script>
      for(let elem of document.querySelectorAll('*')) {
        elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
        elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
      }
    </script>
  </body>
</html>

Also, there is a specific property, such as event.eventPhase telling the number of the phase on which the event was caught. This property is also rarely used, because, as a rule, you learn about it in the handler.

For removing the handler, the same phase is necessary for the removeEventListener. In case you add addEventListener(..., true), the same phase should be mentioned in removeEventListener(..., true) to remove the handler correctly.

The listeners of the same element and same phase may run in their set order. For example, in the event of having multiple event handlers on the same phase that are assigned to the same element with addEventListener, they will run in the same order as they are generated. Here is an example:

elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));

Summary

The processes of capturing and bubbling are means of event propagation in the HTML DOM API when an event happens inside an element within another element, and both of the elements have registered a handler.

So, when an event occurs, the most nested element in which it happened becomes the “target element” (event.target). Afterward, this event moves down from the document root to the target element, calling the handlers assigned with addEventListener(...., true) on the path. Then the element bubbles up from the target element to the root and calls the handlers assigned with on<event> and addEventListener without applying any third argument.

To make it more precise, let’s consider a real-life example. When an accident happens in the real world, the local authorities are the first to react. Then if it’s needed, the higher authorities get involved in the process. The same is actual for handlers. The code, which sets the handler on a specific element, knows all the details about the element. So, a handler on a specific <td> might be matched with the exact <tld>. Then its immediate parent gets the information and so on up to the very top element.

The processes of bubbling and capturing lay the foundation for the so-called “event delegation”. The latter is one of the most powerful event handling patterns.




Do you find this helpful?

Related articles