A DOM event travels down the tree (capture), hits the target, then travels back up (bubble). Delegation uses the bubbling phase to handle many elements with one listener.
The three phases
Capture
From
windowdown through each ancestor to the target's parent. Listeners registered with{ capture: true }run here.Target
Listeners on the element itself.
Bubble
Back up from the parent to
window. Default listeners run here. (focus,blur,mouseenterdon't bubble.)
// <div id="outer"><button id="inner">Click</button></div>
outer.addEventListener("click", () => console.log("outer capture"), { capture: true });
outer.addEventListener("click", () => console.log("outer bubble"));
inner.addEventListener("click", () => console.log("button"));
// clicking the button logs: outer capture → button → outer bubbleJavaScript (browser, not run)Event object essentials
| Member | Purpose |
|---|---|
event.target | The element that was actually clicked (deepest) |
event.currentTarget | The element whose listener is running now |
event.preventDefault() | Cancel the default action (follow link, submit form) |
event.stopPropagation() | Stop travelling to further ancestors |
event.stopImmediatePropagation() | Also skip remaining listeners on the same element |
{ once: true }, { passive: true }, { signal } | Auto-remove; promise not to preventDefault (smooth scrolling); remove via AbortController |
Event delegation
// One listener for any number of (even future) <li> items:
document.querySelector("#todo").addEventListener("click", (event) => {
const item = event.target.closest("li[data-id]");
if (!item || !event.currentTarget.contains(item)) return;
if (event.target.matches(".delete")) item.remove();
else item.classList.toggle("done");
});JavaScript (browser, not run)Runnable: EventTarget and CustomEvent
EventTarget, Event and CustomEvent also exist in Node, so the listener API itself can be demonstrated (there is no tree, so no bubbling):
const cart = new EventTarget();
const controller = new AbortController();
cart.addEventListener("add", (e) => console.log("added", e.detail.item), { signal: controller.signal });
cart.addEventListener("add", () => console.log("first add only"), { once: true });
cart.addEventListener("checkout", (e) => {
e.preventDefault();
console.log("checkout blocked by listener");
});
cart.dispatchEvent(new CustomEvent("add", { detail: { item: "book" } }));
cart.dispatchEvent(new CustomEvent("add", { detail: { item: "pen" } }));
controller.abort(); // removes the first listener
cart.dispatchEvent(new CustomEvent("add", { detail: { item: "lamp" } }));
const ok = cart.dispatchEvent(new Event("checkout", { cancelable: true }));
console.log("dispatch returned", ok);added book
first add only
added pen
checkout blocked by listener
dispatch returned falseWhat is event delegation?
target vs currentTarget?