JavaScript Event Delegation & Bubbling
Events bubble up from the target element to the root. This enables efficient event handling.
Event Bubbling
let parent = document.getElementById("parent");
let child = document.getElementById("child");
child.addEventListener("click", () => console.log("Child clicked"));
parent.addEventListener("click", () => console.log("Parent clicked"));
// Clicking child logs both — events bubble up
Stopping Propagation
child.addEventListener("click", (e) => {
e.stopPropagation(); // Stops bubbling
console.log("Child only");
});
Event Target vs Current Target
list.addEventListener("click", (e) => {
console.log(e.target); // The actual clicked element
console.log(e.currentTarget); // The element with the listener
});
Remember: e.target = what was clicked. e.currentTarget = where the listener is.
Event Delegation
Add one listener to the parent instead of one per item:
// Bad: One listener per item
document.querySelectorAll(".item").forEach(item => {
item.addEventListener("click", handler);
});
// Good: One listener on parent
document.getElementById("list").addEventListener("click", (e) => {
if (e.target.classList.contains("item")) handler(e);
});
Why delegation matters: New items added later work automatically — no re-attaching listeners.
Using closest() and matches()
list.addEventListener("click", (e) => {
if (e.target.matches(".item")) handler(e);
});
// Use closest() when clickable element has children
list.addEventListener("click", (e) => {
let item = e.target.closest(".item");
if (!item) return;
handler(item);
});
Tip: closest() is more reliable when the target has nested child elements.
Complete Example: Todo List
let todoList = document.getElementById("todo-list");
let input = document.getElementById("todo-input");
let addBtn = document.getElementById("add-btn");
addBtn.addEventListener("click", () => {
let text = input.value.trim();
if (!text) return;
let li = document.createElement("li");
li.innerHTML = `<span class="text">${text}</span><button class="delete">Delete</button>`;
todoList.appendChild(li);
input.value = "";
});
todoList.addEventListener("click", (e) => {
if (e.target.classList.contains("text")) {
e.target.parentElement.classList.toggle("completed");
}
if (e.target.classList.contains("delete")) {
e.target.parentElement.remove();
}
});
Best Practices
- Use delegation for dynamic content — New elements work automatically
- Use
closest()for parent matching — More reliable with nested elements - Use
matches()for checking — Clean selector matching - Stop propagation when needed — Prevent unintended parent handlers
- Use data attributes — Store extra info on elements
Common Mistakes
- Not checking
e.target— Handle all clicks unintentionally - Forgetting
stopPropagation()— Unintended side effects - Using delegation everywhere — Simple cases don’t need it
- Ignoring keyboard events — Accessibility matters
- Not cleaning up listeners — Remove when no longer needed