Skip to main content
Mohammad Shehadeh — home (MSH monogram, letter M filled with the Palestinian flag)

Event Delegation in JavaScript

Published on
3 min read

You have a list of 200 items, each needing a click handler. The obvious move is to loop over them and attach a listener to every one. It works, until the list grows, until items get added on the fly, until you're babysitting 200 listeners and the page feels sluggish.

There's a better way: attach one listener to the parent and let it handle clicks for every child. That's event delegation, and it rides on a built-in DOM behavior called bubbling.

First, Bubbling

Click an element and the event doesn't stop there. It travels up through every ancestor, from the thing you clicked to its parent, on up to document. That's bubbling.

structure.html
1<ul id="menu">
2  <li>Home</li>
3  <li>About</li>
4  <li>Contact</li>
5</ul>

Click "About" and the event fires on the <li>, then bubbles to the <ul>, then <body>, then document. A listener on any of them hears the click. Delegation puts the listener on the ancestor and lets it catch everything below.

The Naive Way

One listener per item:

without-delegation.js
1const items = document.querySelectorAll("#menu li");
2
3items.forEach((item) => {
4  item.addEventListener("click", (event) => {
5      console.log("Clicked:", event.target.textContent);
6  });
7});

200 items means 200 listeners in memory. Add a new <li> later and it's dead, no listener attached. Remove items and you've got cleanup to worry about. It scales badly in every direction.

The Delegated Way

One listener on the parent. When a click bubbles up, check event.target to see which child was actually clicked.

with-delegation.js
1const menu = document.getElementById("menu");
2
3menu.addEventListener("click", (event) => {
4  const item = event.target.closest("li");
5
6  // Ignore clicks that didn't land on an <li>
7  if (!item || !menu.contains(item)) return;
8
9  console.log("Clicked:", item.textContent);
10});

One listener now handles every item, even ones added later. Two pieces make it work:

  • event.target is the deepest element actually clicked, not the one holding the listener (that's event.currentTarget).
  • closest("li") walks up to the nearest matching ancestor. So if your <li> has an icon inside it, a click on the icon still resolves to the right item.
target vs currentTarget

event.target is what the user clicked. event.currentTarget is the element whose listener is running (the parent). Delegation lives in the gap between the two.

Why It's Better

The listener count stays at one whether you have 5 items or 5,000. New children just work, because the parent was always there. There's nothing to clean up when items disappear. Less code, less memory, fewer bugs.

dynamic-content.js
1const menu = document.getElementById("menu");
2
3menu.addEventListener("click", (event) => {
4  const item = event.target.closest("li");
5  if (!item) return;
6  console.log("Clicked:", item.dataset.id);
7});
8
9// Added later — no extra wiring needed, the delegate handles it
10const newItem = document.createElement("li");
11newItem.dataset.id = "42";
12newItem.textContent = "Settings";
13menu.appendChild(newItem);

A Few Gotchas

Delegation isn't magic, watch for these:

  • Some events don't bubble. focus, blur, mouseenter, and mouseleave won't reach the parent. Use focusin/focusout (which do bubble) instead.
  • stopPropagation() in a child kills the event before it reaches your delegate. Use it deliberately.
  • Don't delegate everything to document. Pick the closest stable parent that wraps the elements you care about.

The Takeaway

Bubbling means a child's click passes through every ancestor, so one listener on the parent can do the work of hundreds. Read event.target (plus closest()) to find what was clicked, and you get less code, less memory, and dynamic content for free.

So next time you're about to loop and attach the same handler to every element, stop. One listener on the parent. Let bubbling do the rest.

Related Articles

GET IN TOUCH

Let's work together

I build fast, accessible, and delightful digital experiences for the web. Whether you have a project in mind or just want to connect, I'd love to hear from you.

Get in touch

or reach out directly at hello@mohammadshehadeh.com