Event Delegation and Bubbling
Answer
Event bubbling is when an event triggered on an element "bubbles up" through its ancestors. Event delegation leverages this by placing a single event listener on a parent to handle events from its children.
Event Propagation Phases
Event Bubbling
document.querySelector(".parent").addEventListener("click", () => {
console.log("Parent clicked");
});
document.querySelector(".child").addEventListener("click", () => {
console.log("Child clicked");
});
// Clicking on .child outputs:
// "Child clicked"
// "Parent clicked" (event bubbled up!)
Event Capturing
// Third parameter: true = capture phase
document.querySelector(".parent").addEventListener(
"click",
() => {
console.log("Parent (capture)");
},
true
);
document.querySelector(".parent").addEventListener(
"click",
() => {
console.log("Parent (bubble)");
},
false
);
document.querySelector(".child").addEventListener("click", () => {
console.log("Child");
});
// Clicking .child outputs:
// "Parent (capture)" - capturing first
// "Child" - target
// "Parent (bubble)" - bubbling last
Stopping Propagation
// Stop bubbling
document.querySelector(".child").addEventListener("click", (e) => {
e.stopPropagation();
console.log("Child clicked - stops here");
});
// Stop immediate - prevents other handlers on same element too
document.querySelector(".child").addEventListener("click", (e) => {
e.stopImmediatePropagation();
});
Event Delegation
Instead of adding listeners to many elements, add one to the parent:
// ❌ Inefficient: Listener on each button
document.querySelectorAll(".btn").forEach((btn) => {
btn.addEventListener("click", handleClick);
});
// ✅ Efficient: One listener on parent
document.querySelector(".button-container").addEventListener("click", (e) => {
if (e.target.matches(".btn")) {
handleClick(e);
}
});
Practical Example
<ul id="todo-list">
<li data-id="1">
<span>Task 1</span>
<button class="delete">Delete</button>
<button class="edit">Edit</button>
</li>
<li data-id="2">
<span>Task 2</span>
<button class="delete">Delete</button>
<button class="edit">Edit</button>
</li>
<!-- More items... -->
</ul>
// One listener handles all current AND future items
document.getElementById("todo-list").addEventListener("click", (e) => {
const target = e.target;
const listItem = target.closest("li");
if (!listItem) return;
const id = listItem.dataset.id;
if (target.matches(".delete")) {
deleteTodo(id);
} else if (target.matches(".edit")) {
editTodo(id);
}
});
// Dynamically added items work automatically!
function addTodo(text) {
const li = document.createElement("li");
li.dataset.id = Date.now();
li.innerHTML = `
<span>${text}</span>
<button class="delete">Delete</button>
<button class="edit">Edit</button>
`;
document.getElementById("todo-list").appendChild(li);
// No need to attach new listeners!
}
Benefits of Event Delegation
Common Patterns
Dropdown Menu
document.querySelector(".dropdown").addEventListener("click", (e) => {
if (e.target.matches(".dropdown-trigger")) {
e.currentTarget.classList.toggle("open");
} else if (e.target.matches(".dropdown-item")) {
selectItem(e.target.dataset.value);
}
});
Tab Navigation
document.querySelector(".tabs").addEventListener("click", (e) => {
const tab = e.target.closest("[data-tab]");
if (!tab) return;
// Remove active from all tabs
document.querySelectorAll("[data-tab]").forEach((t) => {
t.classList.remove("active");
});
// Add active to clicked tab
tab.classList.add("active");
// Show corresponding panel
const panelId = tab.dataset.tab;
document.querySelectorAll(".panel").forEach((p) => {
p.hidden = p.id !== panelId;
});
});
Event Properties
element.addEventListener("click", (e) => {
e.target; // Element that triggered the event
e.currentTarget; // Element the listener is attached to
e.type; // 'click'
e.bubbles; // true
e.eventPhase; // 1=capturing, 2=target, 3=bubbling
// Prevent default behavior
e.preventDefault();
// Stop propagation
e.stopPropagation();
});
When NOT to Use Delegation
// Events that don't bubble:
// - focus / blur (use focusin / focusout instead)
// - mouseenter / mouseleave (use mouseover / mouseout)
// - load, unload, scroll, resize
// For focus events, use the bubbling versions:
container.addEventListener("focusin", handleFocus);
container.addEventListener("focusout", handleBlur);
Key Points
- Events bubble from target up through ancestors
- Capturing happens before bubbling (rarely used)
- Delegation = one listener on parent for many children
- Use
e.targetto identify the actual clicked element - Use
closest()to find parent elements - Works with dynamically added elements
- More efficient than individual listeners