Event Delegation

Course: JavaScript Intermediate

Lesson 11: Event Delegation

Level

Intermediate

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Explain why event delegation is more efficient than attaching listeners to individual elements
  • Use event.target to identify which element was clicked inside a parent
  • Apply event delegation to handle dynamic lists where elements are added after page load

Frontend Development Context

Event delegation is a pattern that solves one of the most common performance problems in frontend JavaScript: attaching too many event listeners. If you render a list of 200 items and attach a click listener to each one, you have 200 separate listeners consuming memory. If new items are added dynamically, you also have to remember to attach listeners to the new elements.

Event delegation sidesteps both problems by attaching a single listener to a parent element and using the event's target to determine which child was interacted with. This is the technique behind many UI frameworks and libraries, and mastering it will immediately improve the quality of your event handling code.

Explanation

Event delegation relies on event bubbling. When you click on a list item <li>, the click event does not stop there — it bubbles up through the DOM: from the <li> to the <ul>, then to the <div>, then to <body>, then to <html>. Each ancestor element gets a chance to handle the same event.

By placing one listener on the parent (<ul> or a container <div>), you can intercept clicks from any child element inside it. The event.target property tells you which specific element was actually clicked.

When using delegation, you often need to filter by element type or attribute. event.target.closest("li") walks up from the clicked element to find the nearest <li> ancestor — this handles the case where the click lands on a child element inside the <li> (like a button or icon).

event.target.matches(selector) returns true if the clicked element matches a CSS selector. This is useful for filtering clicks to only specific elements inside the container.

Delegation is especially powerful for dynamic lists. Because the listener is on the parent (which already exists), it handles clicks on elements added later — you do not need to re-attach listeners every time the list updates.

Code Example

This example demonstrates event delegation on a task list, including dynamic item addition and interaction with event.target.

const taskList = document.querySelector("#taskList");

// One listener on the parent handles all clicks inside it
taskList.addEventListener("click", function (event) {
  // Find the closest <li> in case a child element was clicked
  const item = event.target.closest("li");
  if (!item) return; // clicked outside any list item

  // Check which action was triggered
  if (event.target.matches(".complete-btn")) {
    item.classList.toggle("completed");
    event.target.textContent = item.classList.contains("completed")
      ? "Undo"
      : "Complete";
  }

  if (event.target.matches(".delete-btn")) {
    item.remove();
  }
});

// Dynamically added items are handled by the same listener
function addTask(text) {
  const li = document.createElement("li");
  li.innerHTML = `
    <span>${text}</span>
    <button class="complete-btn">Complete</button>
    <button class="delete-btn">Delete</button>
  `;
  taskList.appendChild(li);
}

addTask("Learn event delegation");
addTask("Build a dynamic list");
addTask("Master the DOM");

Code Explanation

A single click listener is attached to taskList (the <ul>). All clicks on any element inside the list bubble up to this listener, regardless of when the items were added.

event.target.closest("li") finds the nearest <li> ancestor of the clicked element. This is important because the user may click a <button> or <span> inside the <li>event.target would then be the button, not the list item. closest() handles this by walking up the DOM until it finds a match.

event.target.matches(".complete-btn") checks whether the specific element clicked was the Complete button. This allows the single listener to handle different actions on different buttons by inspecting what was actually clicked.

addTask() creates new <li> elements and appends them to the list. Because the listener is on the parent <ul>, it handles these new items automatically. No new listeners need to be attached when the DOM changes.

Pattern Highlights

Positive Pattern: Attach one listener to a stable parent container instead of individual listeners to each child. This scales to large or dynamic lists without performance or maintenance problems.

⚠️ Neutral Note: Always use event.target.closest(selector) rather than checking event.target directly when list items contain child elements. Clicking a child sets event.target to the child, not the item you intended.

Negative Pattern: Do not place the delegated listener on document or body for every list. While this works, it captures every click on the page and requires extra filtering. Attach the listener to the nearest stable ancestor of the items you care about.

Common Mistakes

Mistake 1: Attaching a listener inside a loop for each item

Creating individual listeners for every element instead of delegating.

document.querySelectorAll("li").forEach(li => {
  li.addEventListener("click", handleClick); // 100 listeners for 100 items
});

This works for static lists but creates memory overhead and breaks for dynamically added items. One delegated listener on the parent is cleaner and more robust.

Mistake 2: Not using closest() when items contain child elements

Checking event.target directly and missing clicks on child elements.

listEl.addEventListener("click", function (event) {
  if (event.target.tagName === "LI") { // misses clicks on <button> inside <li>
    event.target.classList.toggle("done");
  }
});

If the user clicks a button inside the <li>, event.target is the button, not the <li>. Use event.target.closest("li") to always find the intended parent item.

Mistake 3: Forgetting to guard against null from closest()

Not checking whether closest() returned null before using the result.

listEl.addEventListener("click", function (event) {
  const item = event.target.closest("li");
  item.classList.toggle("done"); // TypeError if clicked outside any <li>
});

closest() returns null if no matching ancestor is found. Add a guard: if (!item) return; before using the result.

Quick Recap

  • Event delegation attaches one listener to a parent element to handle events from all its children
  • This works because events bubble up through the DOM from the target element to all ancestors
  • Use event.target.closest(selector) to identify the intended item when children may contain nested elements
  • Delegation automatically handles dynamically added elements — no need to reattach listeners after DOM updates

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01
Ant Skills - Event Delegation