← All concepts
DOM & Events

DOM Events and Delegation

A click does not just happen on the element you tapped; it bubbles up through every ancestor, and one listener up top can catch it all.

Definition

Most DOM events fire in phases. The event is first dispatched at the target element (the thing the user actually clicked or touched), and then it bubbles: it fires again on that element's parent, then that parent's parent, and so on up to the document. (There is also an earlier capture phase, which travels down from the document to the target before bubbling starts, but most listeners are written for the bubble phase, which is the default.) Because of bubbling, a listener attached to an ancestor still runs for events that originate on any of its descendants, not only on the ancestor itself. Event delegation takes advantage of this: instead of attaching a separate listener to every child element (which is wasteful, and misses children added later), you attach a single listener to a stable parent and, inside it, check event.target to see which specific descendant the event actually started on. This is why delegation is common for lists, tables, and any UI where items are added or removed dynamically: the parent's one listener keeps working for every item, old and new, without ever needing to be reattached.

Examples

list.addEventListener("click", (event) => {
  if (event.target.matches("button.delete")) {
    event.target.closest("li").remove();
  }
});

One listener on the list handles clicks on every delete button inside it, including buttons in list items added after this listener was attached.

document.addEventListener("click", (e) => console.log("document saw it"));
button.addEventListener("click", (e) => console.log("button saw it"));
// clicking the button logs: "button saw it", then "document saw it"

The event fires on the target first, then bubbles upward, so the button's own listener runs before the document's listener sees the same click.

Common mistakes

  • Attaching a listener directly to a dynamically added element and forgetting elements added later have no listener of their own.
  • Checking event.currentTarget instead of event.target inside a delegated listener, which always points at the parent, not the actual clicked descendant.
  • Forgetting a click can originate on a nested child (like an icon inside a button) so event.target may not be the element you expected; closest() helps find the right ancestor.

Key takeaways

  • Events fire at the target first, then bubble upward through each ancestor in turn.
  • Event delegation attaches one listener to a stable parent instead of one listener per child.
  • Inside a delegated listener, event.target identifies which descendant the event actually started on.
  • Delegation keeps working for elements added to the parent after the listener was attached.
Check your understanding

A page has a single click listener on document, and a user clicks a button nested inside several divs. In what order do the click listeners run, assuming each ancestor also has one?

Where you see this

  • Attaching one click listener to a list or table to handle clicks on any row, including rows added dynamically later.
  • Handling clicks across a whole app with a single listener on a top-level container, routing based on data attributes on the clicked element.
  • Avoiding the memory and performance cost of attaching hundreds of individual listeners to items in a large, changing list.
  • Debugging why a click handler never fires on an element that was added to the page after the listeners were originally set up.

Practice this

Further reading

Previous
Timers and Scheduling
Next
Prototypes and Inheritance