Event bubbling
We've seen that a web page is composed of elements — headings, paragraphs of text, images, buttons, and so on — and that you can listen for events that happen to these elements. For example, you could add a listener to a button, and it will run when the user clicks the button.
We've also seen that these elements can be nested inside each other: for example, a <button>
could be placed inside a <div>
element. In this case we'd call the <div>
element a parent element, and the <button>
a child element.
In this chapter we'll look at event bubbling — this is what happens when you add an event listener to a parent element, and the user clicks the child element.
Prerequisites: | An understanding of HTML and the fundamentals of CSS, familiarity with JavaScript basics as covered in previous lessons. |
---|---|
Learning outcomes: |
|
Introducing event bubbling
Let's introduce and define event bubbling by way of an example.
Setting a listener on a parent element
Consider a web page like this:
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
Here the button is inside another element, a <div>
element. We say that the <div>
element here is the parent of the element it contains. What happens if we add a click event handler to the parent, then click the button?
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
container.addEventListener("click", handleClick);
You'll see that the parent fires a click event when the user clicks the button:
You clicked on a DIV element
This makes sense: the button is inside the <div>
, so when you click the button you're also implicitly clicking the element it is inside.
Bubbling example
What happens if we add event listeners to the button and the parent?
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
Let's try adding click event handlers to the button, its parent (the <div>
), and the <body>
element that contains both of them:
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
You'll see that all three elements fire a click event when the user clicks the button:
You clicked on a BUTTON element You clicked on a DIV element You clicked on a BODY element
In this case:
- the click on the button fires first.
- followed by the click on its parent (the
<div>
element). - followed by the click on the
<div>
element's parent (the<body>
element).
We describe this by saying that the event bubbles up from the innermost element that was clicked.
This behavior can be useful and can also cause unexpected problems. In the next sections, we'll see a problem that it causes, and find the solution.
Video player example
In this example our page contains a video, which is hidden initially, and a button labeled "Display video". We want the following interaction:
- When the user clicks the "Display video" button, show the box containing the video, but don't start playing the video yet.
- When the user clicks on the video, start playing the video.
- When the user clicks anywhere in the box outside the video, hide the box.
The HTML looks like this:
<button>Display video</button>
<div class="hidden">
<video>
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
type="video/webm" />
<p>
Your browser doesn't support HTML video. Here is a
<a href="rabbit320.mp4">link to the video</a> instead.
</p>
</video>
</div>
It includes:
- a
<button>
element. - a
<div>
element which initially has aclass="hidden"
attribute. - a
<video>
element nested inside the<div>
element.
We're using CSS to hide elements with the "hidden"
class set.
The JavaScript looks like this:
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");
btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", () => video.play());
box.addEventListener("click", () => box.classList.add("hidden"));
This adds three 'click'
event listeners:
- one on the
<button>
, which shows the<div>
that contains the<video>
. - one on the
<video>
, which starts playing the video. - one on the
<div>
, which hides the video.
Let's see how this works:
You should see that when you click the button, the box and the video it contains are shown. But then when you click the video, the video starts to play, but the box is hidden again!
The video is inside the <div>
— it is part of it — so clicking the video runs both the event handlers, causing this behavior.
Fixing the problem with stopPropagation()
As we saw in the last section, event bubbling can sometimes create problems, but there is a way to prevent it.
The Event
object has a function available on it called stopPropagation()
which, when called inside an event handler, prevents the event from bubbling up to any other elements.
We can fix our current problem by changing the JavaScript to this:
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");
btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", (event) => {
event.stopPropagation();
video.play();
});
box.addEventListener("click", () => box.classList.add("hidden"));
All we're doing here is calling stopPropagation()
on the event object in the handler for the <video>
element's 'click'
event. This will stop that event from bubbling up to the box. Now try clicking the button and then the video:
Event capture
An alternative form of event propagation is event capture. This is like event bubbling but the order is reversed: instead of the event firing first on the innermost element targeted, and then on successively less nested elements, the event fires first on the least nested element, and then on successively more nested elements, until the target is reached.
Event capture is disabled by default. To enable it you have to pass the capture
option in addEventListener()
.
This example is just like the bubbling example we saw earlier, except that we have used the capture
option:
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick, { capture: true });
container.addEventListener("click", handleClick, { capture: true });
button.addEventListener("click", handleClick);
In this case, the order of messages is reversed: the <body>
event handler fires first, followed by the <div>
event handler, followed by the <button>
event handler:
You clicked on a BODY element You clicked on a DIV element You clicked on a BUTTON element
Why bother with both capturing and bubbling? In the bad old days, when browsers were much less cross-compatible than now, Netscape only used event capturing, and Internet Explorer used only event bubbling. When the W3C decided to try to standardize the behavior and reach a consensus, they ended up with this system that included both, which is what modern browsers implement.
By default almost all event handlers are registered in the bubbling phase, and this makes more sense most of the time.
Event delegation
In the last section, we looked at a problem caused by event bubbling and how to fix it. Event bubbling isn't just annoying, though: it can be very useful. In particular, it enables event delegation. In this practice, when we want some code to run when the user interacts with any one of a large number of child elements, we set the event listener on their parent and have events that happen on them bubble up to their parent rather than having to set the event listener on every child individually.
Let's go back to our first example, where we set the background color of the whole page when the user clicked a button. Suppose that instead, the page is divided into 16 tiles, and we want to set each tile to a random color when the user clicks that tile.
Here's the HTML:
<div id="container">
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
</div>
We have a little CSS, to set the size and position of the tiles:
#container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 100px;
}
Now in JavaScript, we could add a click event handler for every tile. But a much simpler and more efficient option is to set the click event handler on the parent, and rely on event bubbling to ensure that the handler is executed when the user clicks on a tile:
function random(number) {
return Math.floor(Math.random() * number);
}
function bgChange() {
const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
return rndCol;
}
const container = document.querySelector("#container");
container.addEventListener("click", (event) => {
event.target.style.backgroundColor = bgChange();
});
The output is as follows (try clicking around on it):
Note:
In this example, we're using event.target
to get the element that was the target of the event (that is, the innermost element). If we wanted to access the element that handled this event (in this case the container) we could use event.currentTarget
.
Note: See useful-eventtarget.html for the full source code; also see it running live here.
target
and currentTarget
If you look closely at the examples we've introduced in this page, you'll see that we're using two different properties of the event object to access the element that was clicked. In Setting a listener on a parent element we're using event.currentTarget
. However, in Event delegation, we're using event.target
.
The difference is that target
refers to the element on which the event was initially fired, while currentTarget
refers to the element to which this event handler has been attached.
While target
remains the same while an event bubbles up, currentTarget
will be different for event handlers that are attached to different elements in the hierarchy.
We can see this if we slightly adapt the Bubbling example above. We're using the same HTML as before:
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
The JavaScript is almost the same, except we're logging both target
and currentTarget
:
const output = document.querySelector("#output");
function handleClick(e) {
const logTarget = `Target: ${e.target.tagName}`;
const logCurrentTarget = `Current target: ${e.currentTarget.tagName}`;
output.textContent += `${logTarget}, ${logCurrentTarget}\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
Note that when we click the button, target
is the button element every time, whether the event handler is attached to the button itself, to the <div>
, or to the <body>
. However currentTarget
identifies the element whose event handler we are currently running:
The target
property is commonly used in event delegation, as in our Event delegation example above.
Test your skills!
You've reached the end of this article, but can you remember the most important information? To verify you've retained this information before you move on — see Test your skills: Events.
Summary
You should now know all you need to know about web events at this early stage. As mentioned, events are not really part of the core JavaScript — they are defined in browser Web APIs.
Also, it is important to understand that the different contexts in which JavaScript is used have different event models — from Web APIs to other areas such as browser WebExtensions and Node.js (server-side JavaScript). We are not expecting you to understand all of these areas now, but it certainly helps to understand the basics of events as you forge ahead with learning web development.
Next up, you'll find a challenge that will test your understanding of the last few topics.
See also
- domevents.dev
-
A useful interactive playground app that enables learning about the behavior of the DOM Event system through exploration.
- Event reference
-
The main MDN event reference.
- Event order
-
An excellently detailed discussion of capturing and bubbling by Peter-Paul Koch.