Event Bubbling, Capturing & Delegation
When you click a button inside a div, both elements 'see' the event. Events bubble up from child to parent (bubbling) or down from parent to child (capturing). Event delegation uses this to handle events efficiently on dynamic content.
Bubbling & Delegation
- Bubbling — Event fires on target, then parent, then grandparent, up to document (default)
- Capturing — Event fires from document down to target (use: { capture: true })
- event.stopPropagation() — Stop event from bubbling up to parent elements
- Event delegation — Attach ONE listener to parent instead of many to children. Works with dynamic content!
- Why delegation? — 100 list items = 1 listener (on parent), not 100 listeners. Better performance
- event.target — Use to identify WHICH child was clicked in delegated events
Event Delegation Code
// Bubbling example
// <div id="outer">
// <div id="inner">
// <button id="btn">Click</button>
// </div>
// </div>
// Without stopPropagation: clicking btn triggers all 3 listeners
// document.querySelector("#btn").addEventListener("click", () => console.log("Button"));
// document.querySelector("#inner").addEventListener("click", () => console.log("Inner"));
// document.querySelector("#outer").addEventListener("click", () => console.log("Outer"));
// Output: "Button" → "Inner" → "Outer" (bubbles up!)
// stopPropagation — stops bubbling
// document.querySelector("#btn").addEventListener("click", (e) => {
// e.stopPropagation();
// console.log("Only button"); // parent listeners won't fire
// });
// ✅ Event Delegation — ONE listener for ALL items
// <ul id="todo-list">
// <li data-id="1">Task 1 <button class="delete">×</button></li>
// <li data-id="2">Task 2 <button class="delete">×</button></li>
// </ul>
// document.querySelector("#todo-list").addEventListener("click", (event) => {
// // Check what was clicked
// if (event.target.classList.contains("delete")) {
// const li = event.target.closest("li");
// const id = li.dataset.id;
// li.remove();
// console.log(`Deleted task ${id}`);
// }
// });
// Why delegation works for dynamic content:
// Even items added LATER get handled — the listener is on the parent!
// No need to re-attach listeners when adding new items
console.log("Event bubbling and delegation demonstrated");Tip
Tip
Always prefer event delegation over individual listeners. Add one listener to the parent and check e.target. This works for dynamically added elements too, since the parent already has the listener.
Fewer listeners = better perf. Works for dynamic elements.
Common Mistake
Warning
Forgetting to remove event listeners when elements are removed. This causes memory leaks. Use removeEventListener with the same function reference, or use AbortController for cleanup: controller.abort() removes all associated listeners.
Practice Task
Note
Event delegation: (1) Create a todo list with dynamic items. (2) Add ONE click listener on the parent ul. (3) Check e.target to determine which item was clicked. (4) Add delete functionality using event delegation.
Quick Quiz
Key Takeaways
- When you click a button inside a div, both elements 'see' the event.
- Bubbling — Event fires on target, then parent, then grandparent, up to document (default)
- Capturing — Event fires from document down to target (use: { capture: true })
- event.stopPropagation() — Stop event from bubbling up to parent elements