If you've been attaching event listeners to multiple elements individually, there's a better way. Event delegation can make your JavaScript cleaner, faster, and work seamlessly with dynamically added content.
What is Event Bubbling?
When you click an element, the event doesn't just stop there. It bubbles up through the DOM tree, triggering on each parent element.
<div id="parent">
<button id="child">Click me</button>
</div>
document.getElementById('parent').addEventListener('click', () => {
console.log('Parent clicked');
});
document.getElementById('child').addEventListener('click', () => {
console.log('Child clicked');
});
Click the button and you'll see both messages:
Child clicked
Parent clicked
That's bubbling in action.
Event Delegation: The Smart Way
Instead of adding listeners to each child element, add one to the parent:
// Without delegation - multiple listeners
const items = document.querySelectorAll('#list li');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// With delegation - single listener
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
handleClick(e);
}
});
Real Example: Interactive Todo List
Here's a practical todo list using event delegation:
const todoList = document.getElementById('todo-list');
// One listener handles all todos, even future ones
todoList.addEventListener('click', (e) => {
// Toggle complete
if (e.target.classList.contains('todo-text')) {
e.target.classList.toggle('completed');
}
// Delete todo
if (e.target.classList.contains('delete-btn')) {
e.target.parentElement.remove();
}
});
// Add new todos dynamically
addBtn.addEventListener('click', () => {
const li = document.createElement('li');
li.innerHTML = `
<span class="todo-text">${todoInput.value}</span>
<button class="delete-btn">Delete</button>
`;
todoList.appendChild(li);
// No need to attach new listeners!
});
Why Event Delegation Matters
✅ Better Performance: One listener instead of hundreds
✅ Works with Dynamic Content: New elements automatically work
✅ Less Memory: Fewer listeners means less overhead
✅ Easier Maintenance: One place to update your logic
Key Concepts to Remember
event.target vs event.currentTarget:
-
event.target- The element you actually clicked -
event.currentTarget- The element with the listener
Use closest() for nested elements:
todoList.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) {
// Works even if you click nested elements inside <li>
}
});
Stopping propagation (use sparingly):
element.addEventListener('click', (e) => {
e.stopPropagation(); // Stops bubbling
});
Common Pitfall
Don't forget to verify the clicked element is what you expect:
// Better approach
container.addEventListener('click', (e) => {
const button = e.target.closest('button');
if (button && container.contains(button)) {
// Safe to handle
}
});
When Not to Use It
Event delegation isn't always the answer:
- Events that don't bubble (
focus,blur) - Performance-sensitive interactions (rapid mouse movements)
- Single, static elements (just use a direct listener)
Want to Learn More?
This is just scratching the surface. Event delegation becomes even more powerful when you understand all three phases of event propagation, learn advanced patterns with data attributes, and know how to handle edge cases.
Read the full guide here for deeper examples, best practices, common mistakes, and how frameworks like React handle event delegation behind the scenes.
What's your experience with event delegation? Share in the comments below!
Top comments (0)