Delegation keeps dynamic lists and tables fast and simple to wire up.
When an event fires on an element, it travels through the DOM in three phases:
- Capture: from
windowdown to the target's parent. - 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 } to run during capture instead. Most events bubble; some don't, notably focus, blur, mouseenter and mouseleave (use focusin/focusout or pointerover when you need bubbling versions).
Inside a handler:
event.targetis the element where the event originated (maybe a deeply nested<span>).event.currentTargetis the element whose listener is running.
event.stopPropagation() stops the event travelling further. Use it sparingly — it breaks other code (analytics, close-on-outside-click menus) that relies on bubbling. preventDefault() is unrelated: it cancels the default action but lets the event continue.
Event delegation uses bubbling to handle events for many elements with a single listener on a common ancestor. Combined with element.closest(selector), which walks up from the target to the nearest matching ancestor, it looks like this:
list.addEventListener("click", (e) => {
const button = e.target.closest("button[data-action]");
if (!button || !list.contains(button)) return;
...
});Benefits: one listener instead of hundreds, and elements added later work automatically with no extra wiring. Put the action and IDs in data-* attributes so the handler knows what to do.
Example
// Assumes: <ul id="todos"> with items like
// <li data-id="1">Task <button data-action="done">✓</button>
// <button data-action="delete">✕</button></li>
const list = document.querySelector("#todos");
list.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (!button || !list.contains(button)) return; // click elsewhere
const item = button.closest("li");
console.log("listener on:", event.currentTarget.id); // "todos"
if (button.dataset.action === "delete") item.remove();
if (button.dataset.action === "done") item.classList.toggle("done");
});
// Items added later need no new listeners:
const li = document.createElement("li");
li.dataset.id = "99";
li.innerHTML = 'New <button data-action="delete">✕</button>';
list.append(li);Remember
- Events capture down, hit the target, then bubble up
targetis where it started;currentTargetis where the listener is- Delegate with one ancestor listener +
closest()for dynamic content
Watch out for
- Calling
stopPropagation()by habit, which silently breaks other listeners higher up.