Here's a fascinating thing about events. When you click a button, you're not just clicking the button. You're also clicking whatever it's inside of, and whatever that's inside of, all the way up to the document. After an event fires on the element you clicked, it travels upward through all its ancestors, like a bubble rising through water. This is called event bubbling.
<div id="outer">
<button id="inner">Click me</button>
</div>
document.querySelector("#inner").addEventListener("click", () => {
console.log("1. Button clicked");
});
document.querySelector("#outer").addEventListener("click", () => {
console.log("2. Div clicked (the event bubbled up!)");
});
document.addEventListener("click", () => {
console.log("3. Document clicked (still bubbling!)");
});
Click the button, and all three messages appear, in that order: button first, then the div, then the document.
(If you ever need to stop an event from bubbling any further, you can call event.stopPropagation(). But you'll rarely need it.)
Event delegation
Bubbling might seem like a weird curiosity, but it enables a really powerful trick called event delegation.
Imagine you have a to-do list, and each item has a delete button. You could add a separate click listener to every single button. But what about new items that get added later? They wouldn't have listeners, so you'd have to remember to add one each time. That gets messy.
Instead, put one listener on the parent list. Because clicks bubble up, any click on any button inside the list will reach the parent. Then use event.target to figure out what was actually clicked:
<ul id="todo-list">
<li>Buy plantain <button class="delete">❌</button></li>
<li>Call Grandma <button class="delete">❌</button></li>
<li>Learn JavaScript <button class="delete">❌</button></li>
</ul>
const list = document.querySelector("#todo-list");
list.addEventListener("click", (event) => {
if (event.target.classList.contains("delete")) {
const item = event.target.closest("li");
item.remove();
}
});
One listener handles every delete button, including any buttons for items that are added later. When a click happens anywhere in the list, we check: was it on a .delete button? If so, we use closest("li") to climb up the family tree to the list item that contains the button, and remove it.
Event delegation is like having one receptionist at the front desk of an office building, instead of a separate doorman for every single office. Every visitor passes through the lobby, and the receptionist directs them to the right place. When a new office opens, you don't need to hire a new doorman. The receptionist already handles it.