JavaScript course menu
Event Bubbling, Capturing and Event Delegation in JavaScript
Understand how events travel through the DOM: the capture, target and bubble phases, event.target versus currentTarget, stopPropagation, and event delegation, the pattern of handling events for many or dynamically added elements with one listener.
On this page
When you click a button inside a list inside a page, you have also clicked the list and the page. JavaScript models this by sending each event on a journey through the DOM. Understanding that journey unlocks one of the most useful patterns in front-end code: event delegation.
Bubbling#
After an event happens on an element (the target), it bubbles up through each ancestor in turn, triggering their listeners for the same event:
<div id="outer" style="padding: 16px; background: #fde7c8">outer
<div id="inner" style="padding: 16px; background: #c9f0d3">inner
<button id="btn">Click me</button>
</div>
</div>
<pre id="log"></pre>const log = document.querySelector("#log");
for (const id of ["btn", "inner", "outer"]) {
document.querySelector(`#${id}`).addEventListener("click", (event) => {
log.textContent += `${id} heard it (target: ${event.target.id})\n`;
});
}Click the button: the log shows btn, then inner, then outer. All three listeners ran, and event.target was the button every time. Click the green area instead, and only inner and outer respond.
The three phases#
An event actually travels in three phases:
- Capture: from
windowdown through the ancestors to the target. - Target: at the element where it happened.
- Bubble: back up from the target to
window.
Listeners run in the bubble phase by default. Pass { capture: true } as the third argument to run in the capture phase instead, which is rarely needed.
Most events bubble. A few do not, notably focus, blur, mouseenter and mouseleave; their bubbling versions are focusin, focusout, mouseover and mouseout.
target versus currentTarget#
Stopping propagation#
event.stopPropagation() stops the event from travelling further up:
modal.addEventListener("click", (event) => {
event.stopPropagation(); // clicks inside the modal do not reach the backdrop
});Use it sparingly: other code (analytics, menus that close when you click outside them) may rely on events reaching the document. Do not confuse it with preventDefault(), which cancels the browser's default action but lets the event keep bubbling.
Event delegation#
Instead of adding a listener to every item, add one listener to their common parent and use event.target to find which item was clicked:
<ul id="todo">
<li>Learn bubbling <button class="delete">✕</button></li>
<li>Learn delegation <button class="delete">✕</button></li>
</ul>
<button id="add">Add item</button>const list = document.querySelector("#todo");
let n = 1;
list.addEventListener("click", (event) => {
const button = event.target.closest(".delete");
if (!button || !list.contains(button)) return; // the click was not on a delete button
button.closest("li").remove();
});
document.querySelector("#add").addEventListener("click", () => {
const li = document.createElement("li");
li.innerHTML = `New item ${n++} <button class="delete">✕</button>`;
list.append(li);
});Add some items and delete them: delete buttons on new items work too, even though no listener was ever attached to them. The list's single listener handles every click inside it.
Why delegation is useful:
- Dynamic content: works for elements added later, with no extra code.
- Fewer listeners: one listener instead of hundreds for long lists or tables.
- Simpler cleanup: nothing to remove when items are deleted.
The key line is event.target.closest(".delete"): the click may land on an icon or text inside the button, so closest walks up to find the button itself (Selecting Elements).
Practice#
Practice exercise
Make a row of colour buttons (each with a data-colour attribute) inside one container, and use a single delegated listener on the container to set the background of a preview box to the clicked colour.
Show solution
<div id="palette">
<button data-colour="tomato">Tomato</button>
<button data-colour="seagreen">Sea green</button>
<button data-colour="royalblue">Royal blue</button>
</div>
<div id="box" style="height: 60px; margin-top: 10px; border: 1px solid #ccc"></div>document.querySelector("#palette").addEventListener("click", (event) => {
const button = event.target.closest("button[data-colour]");
if (!button) return;
document.querySelector("#box").style.background = button.dataset.colour;
});Key takeaway#
Finished this lesson?