JavaScript Event Bubbling: A Comprehensive Guide

John John (304)
5 minutes

As a JavaScript developer, one of the most common things you'll need to do is react to the user's events. For example, the user clicks submit on a form and you need to validate it, or the user clicks a button that dynamically adds content to the page. There are many times when the user will interact with your application and you'll need to respond to their actions. And since DOM elements are always nested within each other, managing the events and targets can be tricky. Enter event bubbling.

Posted in these interests:
h/javascript27 guides
h/webdev60 guides

We often nest related DOM elements when we write our markup. For instance, we may have an div containing an img, an h2, and a p tags

<div>
    <img src="..." />
    <h2>My item</h2>
    <p>Item description</p>
</div>

Suppose we wanted to allow the user to click anywhere inside of this div. If we were simply navigating the user to a new page, an a tag with a properly set href would be sufficient. But if we want to perform some other action on the current page, we'll want to use JavaScript to listen for click events to the entire div and all of it's children.

Fortunately, we don't have to listen and respond to a click on each DOM element - div, img, h2, and p. We only have to listen to events on the parent! This is due to event bubbling.

When an event occurs on any DOM element on a page, the event is bubbled up through it's parent elements triggering the event on each. So in our example above, if a user clicks on the p element, the click event will be triggered on the p followed by the parent div following by the parent of the div all the way to the document object.

As described in the previous step, the event is first triggered on the target element and then on it's parents moving up the chain. We can prove this with a quick example.

Suppose we have the following markup:

<div id="outer">
    <div id="middle">
        <div id="inner"></div>
    </div>
</div>

We can set a click event listener on each div like this:

$(function() {
  $("#outer").on('click', function() {
    console.log('#outer click event triggered');
  });
  $("#middle").on('click', function() {
    console.log('#middle click event triggered');
  });
  $("#inner").on('click', function() {
    console.log('#inner click event triggered');
  });
});

If I click on the inner div, #inner, I will see this in the console logs:

#inner click event triggered
#middle click event triggered
#outer click event triggered

Even though it appears that they are logged at the same time, the order is important. The event was triggered first on the inner div, followed by the middle div, followed by the outer div.

Suppose I click on the middle div.

#middle click event triggered
#outer click event triggered

You probably assumed the output. The middle div was triggered first followed by it's parent outer div. The event on the inner div wasn't triggered.

In JavaScript, events have a target attribute to identify which DOM element was the target of the event. When we listen for events on a specific DOM element we can provide a callback function that is called when the event is triggered. This callback function takes the event as its first argument. This event object contains the target of the event. Here's a quick example showing how to access the target.

The markup:

<button>Click me</button>

And the JavaScript:

$(function() {
    $("button").on('click', function(event) {
        console.log(event.target);
    });
});

Note: Even though we are using jQuery, as it stands the event.target attribute will be a vanilla JavaScript DOM object, not a jQuery object.

In the above example, the target would be the actual button that was clicked.

When we get into nested DOM elements this becomes a little bit more interesting. Take our example from the previous step. Suppose we log the target for each div.

$(function() {
  $("#outer").on('click', function(e) {
    console.log($(e.target).attr('id'), $(this).attr('id'));
  });
  $("#middle").on('click', function(e) {
    console.log($(e.target).attr('id'), $(this).attr('id'));
  });
  $("#inner").on('click', function(e) {
    console.log($(e.target).attr('id'), $(this).attr('id'));
  });
});

The above code simply logs the id of the target and the id of this - this being the element we're listening to. A click on the #inner element will log this:

inner inner
inner middle
inner outer

Each line shows target this.

As expected, the target doesn't change. Even though the event is bubbled up and triggered on parent elements, the target remains the actual element that was clicked on. However, this provides access to the element we're listening to. As we see, inner inner is first showing that the event on the #inner element was triggered first, and obviously #inner is the target as well.

Sometimes you'll want to stop an event from bubbling all the way to the top. Maybe you know for sure you're finished processing the event, or perhaps in some case you don't want the event to be triggered on the parent.

We can stop bubbling with event.stopPropagation().

Taking the example we're using throughout this guide, we can test this out:

$(function() {
  $("#outer").on('click', function(e) {
    console.log("#outer click event triggered");
  });
  $("#middle").on('click', function(e) {
    console.log("#middle click event triggered");
  });
  $("#inner").on('click', function(e) {
    console.log("#inner click event triggered");
    e.stopPropagation();
  });
});

This is similar to what we saw before except that we've added e.stopPropagation() to the callback for the #inner div. Before, a click on the #inner element bubbled up to the parent elements and logged 3 lines, but this time it stops with the target and logs:

#inner click event triggered
Got room in your schedule for this project?
Ash Ash (362)
30 minutes

You'll never need to buy another calendar again. This is a Raspberry Pi calendar that can sit on your desk, nightstand, or even be mounted on the wall.