Event Delegation

Attaching one listener to a stable ancestor, using event bubbling to handle events from any current or future descendant.

                ancestor.addEventListener(type, (e) => { const match = e.target.closest(selector); })
              

Purpose

Event delegation attaches a single listener to a stable ancestor element instead of one listener per descendant, relying on bubbling (recall addEventListener and the Event Object) to handle events from any current or future descendant with no extra wiring.

Example

const list = document.querySelector('#task-list');

list.addEventListener('click', (event) => {
  const button = event.target.closest('button[data-action]');
  if (!button) return; // click didn't originate on/inside a relevant button
  const action = button.dataset.action;
  // handle action…
});

Behavior notes

  • event.target is the exact element the event originated on, which may be a descendant of the element actually of interest (an icon inside a button, for instance) — .closest(selector) walks up from event.target to find the nearest matching ancestor, or the element itself.
  • A guard clause (if (!button) return;) is essential — the delegated listener fires for every click inside the ancestor, including ones that don’t match anything relevant.
  • Elements added to the ancestor after the listener was attached are handled automatically, since the listener isn’t tied to any specific descendant.

Common mistakes

  • Forgetting the guard clause, and running action logic for clicks that didn’t actually originate on a relevant element.
  • Delegating to document when a much closer, stable ancestor would work — unnecessarily wide delegation makes the guard-clause matching logic do more work than needed.

Runtime notes

Built on core DOM event bubbling — browser-only.

Taught in these lessons

Related reference

Updated 2026-09-03