Imagine a list with 100 items, each with a delete button. You could attach 100 listeners. Or you could attach one to the list, and let bubbling bring the clicks to it. That is event delegation.
<ul id="tasks">
<li>Wash up <button data-action="delete">Delete</button></li>
<li>Study <button data-action="delete">Delete</button></li>
</ul>
<button id="add">Add task</button>
<script>
const tasks = document.querySelector("#tasks");
tasks.addEventListener("click", event => {
const button = event.target.closest("[data-action='delete']");
if (!button) return; // the click was elsewhere
button.closest("li").remove();
});
document.querySelector("#add").addEventListener("click", () => {
const li = document.createElement("li");
li.innerHTML = 'New task <button data-action="delete">Delete</button>';
tasks.append(li);
});
</script>
Add a task and delete it: it works even though the new button never had its own listener.
How it works
- The listener sits on the parent (
#tasks). - A click on a child bubbles up to it.
- In the handler,
event.targettells you which child was clicked. closest()finds the element you care about, even if the user clicked something inside it (an icon inside the button, for example).- If it is not what you want,
returnearly.
Why use it
- Fewer listeners. One instead of hundreds, which uses less memory.
- Works for elements added later, which is perfect for lists built from data.
- Less cleanup. Removing an element does not leave a listener behind.
Use closest, not target
event.target is the innermost clicked element, which might be an <svg> or <span> inside your button. target.closest("button") handles that.
const button = event.target.closest("button");
if (!button || !list.contains(button)) return;
The contains check makes sure the match is inside your container.
Telling items apart
Use data- attributes to carry information, see Data Attributes.
<div id="toolbar">
<button data-tool="bold">B</button>
<button data-tool="italic">I</button>
</div>
<p id="out"></p>
<script>
document.querySelector("#toolbar").addEventListener("click", event => {
const tool = event.target.closest("[data-tool]")?.dataset.tool;
if (tool) document.querySelector("#out").textContent = "Tool: " + tool;
});
</script>
When not to use it
For events that do not bubble (focus, blur, mouseenter), use the bubbling alternatives, or attach listeners directly. And for a single, one-off element, a normal listener is simpler.