Event Bubbling and Event Capturing in JavaScript

Events are bound to happen all the time in a web application or web page. Events tell the page how to behave when something happens. For instance, if a user clicks a button, the web page should react, and events are used to define this reaction. Events can be anything, but here are the most common:

  • A user clicks or hovers on an element.
  • A user types something on the keyboard.
  • A user chooses to close or open a window.
  • A user resizes a window.
  • The web page starts or stops loading.
  • A form is submitted.
  • An error occurs.

An event handler is called when an event occurs. In many cases, event handlers are also known as event listeners. 

The moment you start to make use of event listeners, you will come across a few interesting browser behaviours. In this post, we will discuss two such behaviours: event bubbling and event capturing

Event Bubbling

Let’s start with a very simple example.

1
<div onclick="console.log('Click Event')">
2
  <span>If you click here, I will print <b>Click Event</b></span>
3
</div>

In the above piece of code, a handler is assigned to div. However, if you click on any one of the nested tags, span or b, you will see an entry in the console.

Isn’t this strange? Why would the handler of div be called when span or b is clicked?

The concept which best describes this behaviour would be bubbling

event bubblingevent bubblingevent bubbling

Bubbling describes when an event happens on a specific element and then continues to its parent. The event can bubble all the way up to its ancestors. In our case, the event started at span and travelled up to its parent, the div. As seen in the diagram above, it will next travel up to the Window.

Why does this happen? What is the science behind this behaviour? To find answers to these questions, keep reading.

Understanding event.target

When an event occurs on an element, the parent will have access to all the details about the event. The parent can identify when and where the event occurred. The parent can make use of the target property in an event to gather this information. And the target property has information about the deeply nested element, which caused the event to happen. In simpler terms:

  • event.target is the element which initiated the entire process.
  • this identifies the current element upon which the event listener runs. this will be equal to event.target when the action was performed on the current element directly. 

How to Stop Bubbling

Prevent Bubbling With event.stopPropagation

Bubbling can be prevented. As mentioned above, bubbling is where an event goes upwards, from the element to the window object. In this path, any element can choose to stop bubbling. The method used to stop bubbling is event.stopPropagation

Now, let’s try to use stopPropagation in our example.

1
<div onclick="console.log('Click Event')">
2
  <span onclick="event.stopPropagation()">If you click here, I will not print <b>Click Event</b></span>
3
</div>

When the user clicks on span, the event will no longer propagate. 

Stop Bubbling With event.stopImmediatePropagation

event.stopPropagation does not work in all cases. The method fails when an element has multiple event handlers for a single event. To solve such cases, another method needs to be used, and that would be event.stopImmediatePropagation

Let’s dive a little deeper. In the code snippet given below, the button1 element has two event handlers. event.stopPropagation inside handler1 will not stop handler2 from executing. 

To stop all the handlers and prevent bubbling on button1, we need to use the event.stopImmediatePropagation method. When event.stopImmediatePropagation is used, neither of the handlers, handler1 or handler2, will be executed. 

1
function handler1() {
2
  console.log(This is Handler 1);
3
}
4

5
function handler2() {
6
  console.log(This is Handler 2);
7
}
8

9
document.getElementById('button1').addEventListener('click',handler1,false);
10
document.getElementById('button1').addEventListener('click',handler2,false);

Should You Stop Event Bubbling?

Event bubbling is convenient. The whole process has been well thought out architecturally. So, if you are going to stop bubbling, there must be a valid reason.

Methods like event.stopPropagation have a few drawbacks. Here’s a common one: suppose we create a simple nested menu and then add a click listener on each submenu item. If each submenu item has event.stopPropagation, then the architecture is less flexible. For example, if you want to have some behaviour at the menu level that occurs whenever any submenu item is clicked, you won’t be able to implement it by adding an event listener to the parent menu. Similarly, you won’t be able to catch clicks at the window level for analytics or verification.

Remember, most of the time it’s not necessary to stop bubbling. There are better ways to solve the problems that might occur with bubbling. For example, you could use custom events. Or you could use data inside the event to help handlers decide what needs to be done next. 

Exceptions to Bubbling

Bubbling holds true in most cases. However, for some events, bubbling does not happen. Here’s a list of events that do not bubble:

  • blur (focusout is the same as blur, but it bubbles)
  • focus (focusin is the same as focus, but it bubbles)
  • mouseLeave (mouseOut is the same as mouseLeave, but it bubbles)
  • mouseEnter (mouseOver is the same as mouseEnter, but it bubbles)
  • load, unload, error, abort, and beforeUnload do not bubble

If you want to confirm whether an event bubbles or not, take a look at the Event created. If the bubbles property is true, the event will bubble.

Event Capturing

Another interesting concept in events is capturing. If you take a good look at code, capturing is not used very often. But it is good to learn and understand it better. 

Capturing is also known as trickling. It is where an event goes down from the parent to an element. As seen in the diagram below, the event travels from the window to the document, and it can reach a simple <tr>

event capturingevent capturingevent capturing

Once the control flows from the ancestors and reaches the target, it goes up again, and this is called the bubbling phase. Most of the time, capturing is hidden from developers. This is because event handlers are added using two properties: an event and an event listener. The event listeners are often unaware of capturing. They only take care of the current target and the bubbling process. If the target wants to capture an event specifically, the {capture:boolean} attribute has to be set to true

1
//like this:
2
button1.addEventListener(..., {capture: true})
3

4
//or like this:
5
button1.addEventListener(..., true)

The capture variable can have two possible options:

  • If true, the target element will capture any event flowing in from its ancestors. 
  • if false, the target element will bubble any event upwards to its ancestor. This is the default value of the property. 

When an event handler is removed from its target, the correct state should be mentioned for the capture property. For example, if capture was true when the event listener was added, it must be mentioned as true in the remove handler too. 

If an event has multiple handlers, the sequence in which capturing happens will depend on the order of creation.

How to Stop Capturing

Stop Capturing With event.stopPropagation

You can use event.stopPropagation to prevent capturing. This method can stop capturing, and it will stop bubbling too.

Stop Capturing With event.stopImmediatePropagation

You can consider event.stopPropagation to be a sibling of event.stopImmediatePropagation. Just like the previous method, stopImmediatePropagation can stop both capturing and bubbling. 

Why Event Capturing and Event Bubbling?

Now, you might wonder why there is both event capturing and event bubbling. In the good old days, before JavaScript was standardized, Netscape used event capturing, and Internet Explorer used event bubbling. Consequently, W3C had to identify a standardised behaviour, which is why all modern browsers support event capturing and event bubbling. By default, event capturing is disabled, and event bubbling is enabled. 

Harness Event Bubbling With the Event Delegation Pattern

Now that we have understood event capturing and bubbling, let’s try to leverage their benefits. Capturing and bubbling combine into a powerful event handling pattern known as event delegation. 

Event delegation is useful when multiple elements need to behave similarly.

In a real-life situation, it can be difficult to assign unique handlers to each element. For example, you might have many elements in a large table, and adding event handlers to each of them can have a significant performance penalty. Or you might be creating and destroying elements dynamically. In these cases, it can be much simpler and more efficient to assign a single handler to the root. Inspecting event.target will provide access to the source of the event. With this piece of information, you can decide what needs to be done. 

The event delegation pattern can help manage event bubbling more effectively. By placing the event listener on a parent or even on the document level, you can handle the event at that level, inspect the event object to determine which element was the original target, and act accordingly. This allows you to control the behaviour more directly and avoid potential conflicts between event handlers on different elements.

Here’s a simple example to help you understand event delegation better. Say we want to create a button to show or hide its child div. A simple way to code this would be to just add a handler for the button. However, if there are many such buttons, it could affect performance, and maintenance would be difficult if the handler is coded or referenced in many places.

On the other hand, with event delegation, you could add a single handler at the root level to show or hide an element defined by a dataset variable in HTML.

The code snippet shown below achieves this perfectly. 

1
<button data-id="child">
2
  Show/Hide Children
3
</button>
4

5
<div id="child" hidden>
6
  I am Child #1
7
</div>
8

9
<script>
10
  document.addEventListener('click', function(event) {
11
    let id = event.target.dataset.id;
12
    if (!id) return;
13

14
    let elem = document.getElementById(id);
15

16
    elem.hidden = !elem.hidden;
17
  });
18
</script>

Conclusion

With every event, an element gets identified as the target element and can be fetched using event.target. If an event moves downwards from the document root to the target, it is called capturing. When an event moves upwards from the target to its ancestors, it is called bubbling. 

To stop event bubbling and capturing, you can use event.stopPropagation or event.stopImmediatePropagation. However, it’s not recommended to make use of these methods. Instead, use a pattern like event delegation to resolve any issues with bubbling or capturing.