How do event bubbling, and event delegation work?

Event bubbling, or propagation, refers to how an event “bubbles up” to parent objects when triggered. For example, consider this code:

<p>
  <span>Click me~!</span>
</p>

The p element here would be considered the parent of the span element.

When you click on the span element, like you are instructed to, the span element becomes the target of a click event. That event, however, also bubbles up to the parent – the p element can receive and consume that event as needed.

But what does this actually mean? Well, you could attach an event listener to the p element:

const p = document.querySelector("p");
p.addEventListener("click", (event) => {
  console.log(event.target);
});

Then, when you click on the span element you will see the text Click me~! logged to the console.

The event propagates to the parent p element, which consumes it in an event listener to display the target of the event.

Notice how the target is still the span element. This is because the span element received the initial click.

Just to be sure how things are working, let’s expand our code:

const p = document.querySelector("p");
const span = document.querySelector("span");
p.addEventListener("click", (event) => {
  console.log("P listener: ");
  console.log(event.target);
});
span.addEventListener("click", (event) => {
  console.log("Span listener: ");
  console.log(event.target);
});

To give you an idea of how the event bubbles up, here’s what you’ll see in the console after clicking the span element:

"Span listener: "
<span>Click me~!</span>
"P listener: "
<span>Click me~!</span>

Now let’s see what happens when you prevent the propagation of an event with stopPropagation(). We’ll call it in our span’s event listener:

const p = document.querySelector("p");
const span = document.querySelector("span");
p.addEventListener("click", (event) => {
  console.log("P listener: ");
  console.log(event.target);
});
span.addEventListener("click", (event) => {
  console.log("Span listener: ");
  console.log(event.target);
  event.stopPropagation();
});

And then click our span again:

"Span listener: "
<span>Click me~!</span>

This time, we don’t see our p listener trigger. The event never fires for the p element, because we told it to stop propagation while it was being processed for the child span element.

Event delegation can be thought of as the opposite. It’s the process of taking a captured event, and delegating it to another element.

Going back to our code, let’s update it so clicking on a span element changes it to red:

const p = document.querySelector("p");
const span = document.querySelector("span");
p.addEventListener("click", (event) => {});
span.addEventListener("click", (event) => {
  event.target.style.color = "red";
});

But what if you have twenty span elements? Or maybe you use JavaScript to create more span elements on the fly?

Rather than having to attach an event listener to every single span element, you can actually use the listener on the p element for all of them. In other words, you can delegate the handling of the span clicks to the parent p element.

Our code might now look something like this:

const p = document.querySelector("p");
p.addEventListener("click", (event) => {
  event.target.style.color = "red";
});

Notice how we no longer have any listener attached to the span element at all. You have properly delegated the event handling to the p element. But does it work?

Let’s generate a few extra span elements and see:

<p>
  <span>Click me~!</span>
  <span>Click me~!</span>
  <span>Click me~!</span>
  <span>Click me~!</span>
</p>

Now, each time we click on a span, that element’s text will become red.

And just like that, with a single event listener we’ve properly allowed a click event to bubble up from span elements to the parent p, and delegated the logic for that click event to the p element.

Event propagation and delegation can be a complex topic, especially as you get into heavily nested elements like tables. You are encouraged to explore this further and experiment with some of the code we’ve written here.