Event Delegation
Event delegation is a pattern where you listen on a parent element and handle events from its children.
It works because many events bubble.
The Problem
Imagine a list with several delete buttons.
<ul id="todo-list">
<li>Learn events <button class="delete">Delete</button></li>
<li>Practice DOM <button class="delete">Delete</button></li>
<li>Build a project <button class="delete">Delete</button></li>
</ul>You could add a listener to every button.
const buttons = document.querySelectorAll(".delete");
buttons.forEach((button) => {
button.addEventListener("click", () => {
button.closest("li").remove();
});
});This works, but it has two downsides:
- many listeners may be created for large lists
- buttons added later will not automatically have listeners
Delegating to the Parent
Instead, listen on the parent list.
const list = document.querySelector("#todo-list");
list.addEventListener("click", (event) => {
if (!event.target.matches(".delete")) {
return;
}
event.target.closest("li").remove();
});When a delete button is clicked, the click bubbles to the list. The list listener
checks whether the original target matches .delete.
Using closest
Sometimes the click target is inside the button.
<button class="delete">
<span>Delete</span>
</button>If the user clicks the span, event.target.matches(".delete") is false.
Use closest to search upward from the target.
list.addEventListener("click", (event) => {
const deleteButton = event.target.closest(".delete");
if (!deleteButton || !list.contains(deleteButton)) {
return;
}
deleteButton.closest("li").remove();
});The contains check keeps the handler scoped to this list.
Works with Dynamic Elements
Delegation is especially useful when elements are added later.
const item = document.createElement("li");
item.innerHTML = 'New task <button class="delete">Delete</button>';
list.append(item);The new delete button works without adding another listener because the listener is on the list.
Delegating Different Actions
You can use data attributes to describe actions.
<div id="toolbar">
<button data-action="save">Save</button>
<button data-action="preview">Preview</button>
<button data-action="publish">Publish</button>
</div>const toolbar = document.querySelector("#toolbar");
toolbar.addEventListener("click", (event) => {
const button = event.target.closest("[data-action]");
if (!button) {
return;
}
const action = button.dataset.action;
if (action === "save") {
saveDraft();
} else if (action === "preview") {
showPreview();
} else if (action === "publish") {
publishPost();
}
});This keeps one listener in charge of a related group of controls.
When Delegation Is a Good Fit
Use delegation when:
- many similar child elements need the same behavior
- children may be added or removed dynamically
- a parent naturally owns the interaction
- you want fewer listener registrations
When Direct Listeners Are Better
Direct listeners may be clearer when:
- there is only one element
- the handler is very specific to that element
- the event does not bubble
- the parent would need complicated checks
modalCloseButton.addEventListener("click", closeModal);Simple direct code is often better than clever delegation.
Common Mistakes
Forgetting to filter the target:
list.addEventListener("click", () => {
// This runs for every click anywhere in the list.
});Using event.target when nested elements are possible:
if (event.target.matches(".delete")) {
// May fail if the user clicks an icon inside the button.
}Delegating too high in the document:
document.addEventListener("click", handleEveryClick);Global listeners can be useful, but too many can make interactions hard to debug.
Summary
Event delegation uses bubbling to handle child events from a parent listener.
It is especially useful for lists, menus, tables, toolbars, and dynamic content.
Use closest and careful target checks to keep delegated handlers reliable.