What is event delegation and why is it useful?

Advanced12 min interview
Skills tested:
Event bubbling mechanics and how events travel up the DOM treeUsing e.target vs e.currentTarget to identify the source elementUsing e.target.closest() to handle nested child elements correctlyHandling dynamically added elements without re-attaching listenersIdentifying non-bubbling events and their alternatives

Advertisement

🧩 Scenario

In a real codebase, event delegation is essential for dynamic lists, tables, and any UI where items are added and removed at runtime. Attaching one listener to a container is also more memory-efficient than attaching dozens of individual listeners when the list grows large.

Architecture Walkthrough

How Event Bubbling Powers Delegation

When a user clicks an element, the browser fires the event on the target element first and then propagates it upward through every ancestor in the DOM tree until it reaches document. This bubbling phase is what delegation relies on. Because the click eventually reaches the parent container, a single listener on the container receives every click that starts inside it.

Attaching one listener to a <ul> instead of one listener per <li> means the number of listeners stays constant no matter how many items exist. It also means items added to the list after the page loads are automatically covered without any re-attachment logic.

Identifying the Clicked Child

Inside a delegated handler, e.currentTarget is the element with the listener (the parent). e.target is the element the user actually clicked. To filter only specific children, check whether e.target matches a CSS selector using e.target.matches('.delete-btn') or whether it is contained in a matching ancestor using e.target.closest('.list-item').

closest() is the safer choice when elements have nested markup inside them. If a button contains a <span> for an icon, clicking the icon sets e.target to the span, not the button. e.target.closest('button') walks up from wherever the click landed and finds the nearest button ancestor, making the handler resilient to nested content.

Events That Do Not Bubble

Not all events bubble. focus, blur, mouseenter, and mouseleave do not bubble by default, which means a delegated listener on a parent will never fire for them. The alternatives are focusin and focusout (which do bubble) or passing { capture: true } as the third argument to addEventListener to intercept events during the capture phase before they reach the target.


Key Code Explained

const list = document.querySelector('.todo-list');

// Single delegated listener on the parent
list.addEventListener('click', (e) => {
  // Handle nested elements: find the closest item regardless of what was clicked
  const item = e.target.closest('.todo-item');
  if (!item) return; // click landed outside any item

  if (e.target.matches('.delete-btn')) {
    item.remove();
    return;
  }

  if (e.target.matches('.complete-btn')) {
    item.classList.toggle('completed');
  }
});

// Items added later are covered automatically — no new listener needed
function addItem(text) {
  const li = document.createElement('li');
  li.className = 'todo-item';
  li.innerHTML = `
    <span>${text}</span>
    <button class="complete-btn">Done</button>
    <button class="delete-btn">Delete</button>
  `;
  list.appendChild(li);
}

The e.target.closest('.todo-item') call is the key defensive pattern. Without it, clicking directly on the <span> text would cause e.target to be the span, the matches() checks would return false, and the handler would silently do nothing even though the user clearly clicked an item.


Tradeoffs

ApproachProCon
Delegation (parent)One listener, works for dynamic elementsRequires e.target filtering; bubbling can be disrupted
Direct listenersSimple, no filtering neededMemory grows with element count; must re-attach for new elements
Capture phaseWorks for non-bubbling events like focusLess common, may interfere with other capture-phase handlers

What Interviewers Actually Check

  • Whether you can explain event bubbling and why it enables delegation
  • Whether you know the difference between e.target and e.currentTarget
  • Whether you handle nested HTML with closest() rather than just matches()
  • Whether you can name events that do not bubble and know the alternatives
  • Whether you understand when delegation adds complexity rather than reducing it

Follow-Up Questions

  1. How would you implement delegation for focus events on inputs inside a form?
  2. If a child element calls e.stopPropagation(), how does this affect a delegated listener on the parent?
  3. How does React's synthetic event system implement delegation internally, and how did this change in React 17?
  4. What is the difference between attaching a listener in the bubble phase vs the capture phase?
  5. When would you choose direct listeners over delegation even for a large list?

Common Candidate Mistakes

  • Checking e.target.matches() without accounting for nested HTML inside child elements, causing the handler to miss clicks on inner spans or icons
  • Confusing e.target (the element clicked) with e.currentTarget (the element with the listener)
  • Not knowing that focus and blur do not bubble, then being surprised when a delegated focus handler never fires
  • Forgetting that e.stopPropagation() inside any child prevents the event from reaching a delegated ancestor listener
  • Applying delegation to every list regardless of whether elements are dynamic, adding unnecessary complexity for static content

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you explain the bubbling phase and why it enables delegation?
  • Can you write a delegated click handler using e.target.closest() to handle nested content correctly?
  • Can you explain the difference between e.target and e.currentTarget?
  • Can you name two events that do not bubble and describe the workaround for each?
  • Can you explain what e.stopPropagation() inside a child does to a delegated ancestor listener?

Summary

Event delegation places a single event listener on a parent element and uses the bubbling phase to intercept events from any descendant. The handler identifies which child triggered the event using e.target and filters by CSS selector with matches() or closest(). This reduces the total number of listeners in the DOM, works automatically for dynamically added elements, and simplifies cleanup since only the parent listener needs to be removed.

The practical complexity of delegation lies in handling nested HTML inside the target elements. Using e.target.closest(selector) instead of e.target.matches(selector) handles this correctly by walking up the DOM from wherever the click landed.

Delegation breaks when a child handler calls e.stopPropagation() and cannot be used directly with non-bubbling events like focus and blur. Those events require focusin, focusout, or the capture phase as alternatives.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Does event delegation work for all events?

It works for events that bubble (like click). Some events do not bubble, so you cannot delegate those directly.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement