JavaScript Event Delegation: Complete Tutorial
Learn event delegation in JavaScript: attach one listener to a parent instead of many to each child. Works for dynamically added elements and improves performance.
Event delegation is a pattern where you attach one event listener to a parent element instead of attaching a separate listener to every child. When an event fires on a child, it bubbles up to the parent, and the parent's handler inspects event.target to determine which child was interacted with.
The problem it solves is straightforward. Imagine a list of 100 items, each needing a click handler:
// Bad: 100 separate listeners
const items = document.querySelectorAll("li");
items.forEach((item) => {
item.addEventListener("click", () => {
console.log(item.textContent);
});
});Now imagine those items can change, with new ones added and old ones removed. You need to manage listeners for each change. Event delegation fixes both problems with one listener:
// Good: one listener on the parent
const list = document.querySelector("ul");
list.addEventListener("click", (event) => {
if (event.target.tagName === "LI") {
console.log(event.target.textContent);
}
});One listener handles every <li>, including ones added to the page after the listener is set up.
How Event Bubbling Makes Delegation Possible
When you click an element, the event does not stay on that element. It travels up through every ancestor, triggering any matching listeners along the way. This is called event bubbling.
The diagram shows the path a click takes after you click a list item inside a <ul> inside a <div>. The event starts at the <li>, then fires on the <ul>, then the <div>, then <body>, and finally document. At each stop, any click listener on that element runs.
Event delegation exploits exactly this behavior. You place the listener on an ancestor that you know will be in the DOM, and the bubbling mechanism delivers the event to it.
The Core Pattern: Check event.target
The key to event delegation is inspecting event.target to decide whether to act. Here are the three most common ways to check it:
Check by tag name, the simplest option, but fragile if HTML structure changes:
parent.addEventListener("click", (event) => {
if (event.target.tagName === "BUTTON") {
console.log("A button was clicked.");
}
});Checking tagName works, but it breaks if you later wrap the button in a <span> or change the markup. A CSS selector check with matches() is more flexible because it does not care about the element's tag, only whether it matches the selector you give it:
parent.addEventListener("click", (event) => {
if (event.target.matches(".delete-btn")) {
console.log("Delete button clicked.");
}
});matches() tests whether the element matches a CSS selector. It is clean and works even when the same class appears on different element types.
Find the nearest matching ancestor with closest(), useful when the target might be a child of the element you care about:
parent.addEventListener("click", (event) => {
const card = event.target.closest(".card");
if (card) {
console.log("Card clicked:", card.dataset.id);
}
});closest() walks up from event.target and returns the first ancestor that matches the selector, or null if none is found. Use this when your clickable element contains child elements like icons or text nodes, and a click on a child should still count as a click on the parent.
A Practical Example: Todo List with Delete Buttons
Here is a complete example that shows delegation in action. The listener sits on the list itself and finds the delete button through event.target:
const todoList = document.querySelector("#todo-list");
todoList.addEventListener("click", (event) => {
const deleteBtn = event.target.closest(".delete-btn");
if (!deleteBtn) return;
const todoItem = deleteBtn.closest("li");
todoItem.remove();
console.log("Todo removed.");
});Clicking a delete button removes its parent <li>. The closest(".delete-btn") check means the handler ignores clicks anywhere else in the list. Now add a function that creates new todo items after the page has already loaded:
function addTodo(text) {
const li = document.createElement("li");
li.innerHTML = `${text} <button class="delete-btn">Delete</button>`;
todoList.appendChild(li);
}
addTodo("New dynamic item");The delete button on this new item works immediately, even though it did not exist when the listener was set up. No need to attach a separate listener for it.
When Event Delegation Works (and When It Does Not)
Event delegation depends on event bubbling. It works for events that bubble:
| Event Type | Bubbles? | Delegation Works? |
|---|---|---|
click | Yes | Yes |
keydown / keyup | Yes | Yes |
change | Yes | Yes |
submit | Yes | Yes |
mouseover / mouseout | Yes | Yes |
focus | No | No, use focusin instead |
blur | No | No, use focusout instead |
scroll | No on elements, yes on document | Limited |
For focus and blur, use focusin and focusout which do bubble and work with delegation.
Delegating Keyboard Events
Event delegation works with keyboard events too. Here is a table where pressing Enter on any row highlights it:
const table = document.querySelector("table");
table.addEventListener("keydown", (event) => {
const row = event.target.closest("tr");
if (!row || event.key !== "Enter") return;
row.classList.toggle("highlighted");
console.log(`Toggled row: ${row.rowIndex}`);
});This pattern is useful for accessible list navigation, data tables, and any scenario where keyboard users need to interact with repeating elements.
Event Delegation vs Individual Listeners
| Event Delegation | Individual Listeners | |
|---|---|---|
| Number of listeners | One per parent | One per child |
| Dynamic elements | Handled automatically | Need manual setup |
| Memory usage | Lower for many elements | Higher for many elements |
| Complexity | Requires target checking | Simple and direct |
| Best for | Lists, tables, grids, dynamic content | Fixed, small sets of elements |
For a page with five buttons, individual listeners are fine. For a product grid with 200 items, a search result list, or any interface where elements are added and removed, event delegation is the right choice.
Event delegation builds on two fundamental concepts: attaching listeners and understanding how events bubble. If you are unfamiliar with either, read /javascript/how-to-add-event-listeners-in-js-complete-guide and /javascript/handling-click-events-in-javascript-full-guide first.
Common Mistakes
Forgetting that event.target may be a child element. If your button contains an icon or text node, clicking the icon makes event.target the icon, not the button. Always use closest() when your clickable element has children:
// Wrong: breaks when the button has an icon inside
if (event.target.classList.contains("btn")) { ... }
// Right: works regardless of which child was clicked
const btn = event.target.closest(".btn");
if (btn) { ... }Not checking event.target at all. If your delegated listener runs for every click on the parent without checking event.target, it fires on clicks between elements, on padding, or on unrelated children. Always guard with an if check.
Using delegation for non-bubbling events. If you try to delegate focus or blur, it will not work. Switch to focusin and focusout or attach listeners directly to the elements that need them.
Rune AI
Key Insights
- Event delegation attaches one listener to a parent instead of many to each child.
- Use event.target to identify which child element triggered the event.
- The matches() method checks if the target matches a CSS selector.
- Delegation automatically works for elements added to the page later.
- Only events that bubble (click, keydown, change) work with delegation.
- Use closest() to find the nearest ancestor that matches a selector.
Frequently Asked Questions
Does event delegation work with all event types?
Is event delegation faster than individual listeners?
How do I stop delegated events from firing on the wrong child?
Conclusion
Event delegation is a pattern where you attach a single event listener to a parent element and use event.target to determine which child triggered the event. It reduces the number of listeners, automatically handles dynamically added elements, and is the foundation for scalable event handling in JavaScript.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.