Topic 9.3
Event Delegation
In one line
Event delegation means putting one listener on a parent instead of one on every child: because events bubble, the parent hears every child's event, uses event.target.closest() to find which child it was, and automatically handles children added later.
Think of it like this
A school receptionist. Instead of giving every classroom its own phone line and person, the school has one reception desk. Every call comes to the desk, the receptionist asks "which class is this about?" and passes the message on. New classrooms built next year need no new phone line: the desk already handles them. Delegation is the reception desk: one listener on the parent handles events for all of its children.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Event delegation
- Handling events for many children with one listener on their parent.
- Container
- The parent element that holds the listener, such as a
<ul>or a<table>. - closest(selector)
- A method that returns the element itself or its nearest ancestor matching the selector, or
null. - contains(node)
- A method that tells you whether a node is inside an element (or is the element itself).
- Dynamic content
- Elements added to or removed from the page after it first loaded.
- data attribute
- A custom
data-*attribute that stores information on an element, read withdataset.
Step by step
01Start with a list
Each item has a delete button. The list could have 3 items or 3,000.
<ul id="todos">
<li data-id="1"><span>Buy milk</span> <button data-action="delete">✕</button></li>
<li data-id="2"><span>Walk the dog</span> <button data-action="delete">✕</button></li>
</ul>02The per-item way (and its problem)
You could loop over every button and add a listener. That's one function per button, and any item added later has no listener at all.
document.querySelectorAll("#todos button").forEach((btn) => {
btn.addEventListener("click", () => btn.closest("li").remove());
});03One listener on the container
Attach one listener to the <ul>. Every click inside the list bubbles up to it.
const list = document.getElementById("todos");
list.addEventListener("click", (event) => {
// which element was really clicked?
});04Find the element you care about
closest walks up from wherever the click landed. If it returns null, the click wasn't on a delete button, so do nothing.
list.addEventListener("click", (event) => {
const button = event.target.closest("[data-action='delete']");
if (!button || !list.contains(button)) return;
const item = button.closest("li");
console.log("delete item", item.dataset.id);
item.remove();
});05New items work for free
Append a new <li> with the same markup and its delete button already works. Nothing was attached to it; the <ul> handles it.
Try it yourself
- 1
Add a third action
In the first example, add a button with
data-action="up"to each item (and to theinnerHTMLof new items). In the listener, when the action isup, move the item before its previous sibling withitem.previousElementSibling?.before(item). Predict: does it work for added items without another listener? - 2
Count the listeners
In the second example, add 50 buttons in a loop before the per-item loop. How many listeners does way 1 create? How many does way 2?
- 3
Break delegation on purpose
In the third example, change
focusintofocusin the listener. Click into a box. Why does nothing turn blue?
Code & diagrams
Item 3 was created after the listener, yet its ✓ button works. The click on the text was ignored because `closest("[data-action]")` returned `null`.
Preview
Console
Nothing logged yet.Expected console output
toggled 1
added 3
toggled 3
deleted 2
items left: 2`blue` was added after the loop ran, so it never got its own listener. Only the delegated listener heard it.
Preview
Console
Nothing logged yet.Expected console output
per-item: red
delegated: red
delegated: blue
The script fires the events itself so the output is the same every time. Click into the boxes yourself: the label of the focused box turns blue, all from two listeners on the form.
Preview
Console
Nothing logged yet.Expected console output
focusin from name
focusout from nameBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Compare target directly
In the to-do listener, write if (event.target.dataset.action === "delete") and put an icon inside the button: <button data-action="delete"><svg>…</svg></button>.
Break #2
Forget the null check
Write event.target.closest("li").remove(); with no if, then click the empty space at the bottom of the list.
Myth vs fact
Myth
Delegation is always faster.
Fact
It saves memory and setup time when there are many children. But the delegated listener runs for every click inside the container, so very hot events (mousemove, pointermove) delegated to a large container can cost more. Measure when it matters.
Myth
You can delegate any event.
Fact
Only events that bubble reach the container in the bubbling phase. For focus/blur use focusin/focusout; for mouseenter/mouseleave use mouseover/mouseout (or a capture listener).
Myth
Frameworks don't use delegation.
Fact
React attaches its listeners to the app's root and dispatches to your components itself. That's delegation, done for you.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
closestmatches the starting element too, soevent.target.closest("button")works whether the target is the button or a child of it. It's supported everywhere modern and is much faster than walkingparentElementin a loop yourself. - ▸
**Disabled buttons don't fire
click** at all, so a delegated listener never hears them; this is usually what you want. Elements withpointer-events: nonepass clicks through to whatever is underneath, and the event's target becomes that element. - ▸
For keyboard accessibility, delegate on real
<button>elements, not clickable<div>s. A button firesclickon Enter and Space automatically, so one delegatedclicklistener covers mouse, touch and keyboard users. - ▸
When removing items, delegation also avoids leaks in older patterns that stored references to per-item listeners. With one listener on the container, removing an
<li>leaves nothing behind to clean up.
Remember this
- 1
Event delegation works because of bubbling (Topic 9.2). A click on any
<li>bubbles up to its<ul>, so a single listener on the<ul>hears clicks on every item. Inside it,event.targetsays exactly where the click started. You delegate the job of listening to an ancestor, often called the container. - 2
The target is often not the element you want. In
<li><span>Milk</span> <button>Delete</button></li>, a click can land on the span, the button, or the<li>itself. Useevent.target.closest(selector)(Topic 8.2): it starts at the target and walks up through its ancestors until one matches, returning that element ornull.const item = event.target.closest("li")finds the item, whatever part of it was clicked. - 3
Always check the result.
closestreturnsnullwhen the click wasn't inside a match at all (for example, on the padding of the<ul>), so start withif (!item) return;. Becauseclosestcan also match elements outside the container (an<li>that wraps your whole list in an outer menu), careful code also checkscontainer.contains(item). - 4
Delegation's biggest win is dynamic content: elements added after the listener was attached (new to-do items, search results, rows loaded from a server) work without any extra code, because the listener is on the parent that already exists. With one listener per element, you'd have to remember to attach a listener to every new element and remove it from every deleted one.
- 5
To handle several different actions from one listener, mark buttons with data attributes (Topic 8.4), like
<button data-action="delete">. The listener readsevent.target.closest("[data-action]"), then switches onbutton.dataset.action. The item's own data, likedata-id, tells you which record to change. This keeps the HTML descriptive and the JavaScript in one place. - 6
Delegation needs events that bubble.
click,input,change,keydownandsubmitdo. For focus, delegatefocusin/focusoutinstead offocus/blur; for hovering,mouseover/mouseoutinstead ofmouseenter/mouseleave. Don't delegate everything todocument: a listener there runs for every click on the page. Delegate to the nearest stable container.
Explain it without notes
What is event delegation and why does it work?
Why do you need closest in a delegated listener?
What are the advantages and limits of delegation?
Practice
Given <div id="tabs"><button data-tab="home">Home</button><button data-tab="news">News</button></div>, write one listener that logs open home or open news. Click News from code.
Write a delegated listener on a <table id="grid"> that logs the text of the row (<tr>) whose cell was clicked, and ignores clicks on the <thead>.
Trade-offs
- ↔
Delegation handles any number of current and future children with one listener, but the logic must work out which child was hit; per-element listeners are more direct to read when there are only a few fixed elements.
- ↔
Delegating to
documentcatches everything (handy for global shortcuts), but runs on every event on the page and couples unrelated parts; the nearest stable container is the usual sweet spot.
Done when you can
I can explain why delegation works (bubbling).
I can find the clicked item with
event.target.closest()and ignorenull.I can handle several actions with
data-actionattributes in one listener.I can make elements added later work without new listeners.
I know to use
focusin/focusoutwhen delegating focus.