Introduction
Events don't just fire on the target element—they propagate through the DOM tree. Understanding bubbling and capturing enables powerful patterns like event delegation, which can dramatically simplify your code and improve performance.
Key Concepts
Bubbling: Events propagate from target up to ancestors (default).
Capturing: Events propagate from ancestors down to target (opt-in).
Event Delegation: Handling events on a parent instead of individual children.
Real World Context
Lists with clickable items, dynamic content, menus with many options—delegation handles all of these efficiently with a single listener instead of hundreds.
Deep Dive
Event Propagation Phases
javascript// Three phases: // 1. Capture: window → document → ... → parent → target // 2. Target: event fires on target // 3. Bubble: target → parent → ... → document → window document.body.addEventListener('click', (e) => { console.log('Body clicked (bubble phase)'); }); document.body.addEventListener('click', (e) => { console.log('Body clicked (capture phase)'); }, true); // or { capture: true }
Stopping Propagation
javascriptchild.addEventListener('click', (e) => { e.stopPropagation(); // Parent won't see this event console.log('Child clicked'); }); // stopImmediatePropagation stops other listeners on SAME element too child.addEventListener('click', (e) => { e.stopImmediatePropagation(); }); child.addEventListener('click', (e) => { // This won't run! });
Event Delegation
javascript// BAD: Listener on every item document.querySelectorAll('.item').forEach(item => { item.addEventListener('click', handleClick); }); // GOOD: Single listener on parent document.querySelector('.list').addEventListener('click', (e) => { // Check if clicked element is an item if (e.target.matches('.item')) { handleItemClick(e.target); } // Or find closest matching ancestor const item = e.target.closest('.item'); if (item) { handleItemClick(item); } });
Benefits of Delegation
javascript// Works with dynamically added elements! const list = document.querySelector('.list'); list.addEventListener('click', (e) => { if (e.target.matches('.delete-btn')) { e.target.closest('.item').remove(); } }); // Adding new items - no need to add listeners! list.insertAdjacentHTML('beforeend', ` <li class="item">New item <button class="delete-btn">×</button></li> `); // The delete button already works!
target vs currentTarget
javascriptparent.addEventListener('click', (e) => { e.target; // The actual element clicked (could be a child) e.currentTarget; // Always the element with the listener (parent) // Often different when bubbling! // Click on child: target=child, currentTarget=parent });
Practical Example: Tab Component
javascriptconst tabs = document.querySelector('.tabs'); tabs.addEventListener('click', (e) => { const tab = e.target.closest('[data-tab]'); if (!tab) return; // Remove active from all tabs tabs.querySelectorAll('[data-tab]').forEach(t => t.classList.remove('active')); // Add active to clicked tab tab.classList.add('active'); // Show corresponding content const contentId = tab.dataset.tab; showContent(contentId); });
Common Pitfalls
- Using
e.targetwhen you meane.currentTarget: They differ during bubbling. - Overusing
stopPropagation: Can break other functionality. - Not using
closest(): Directe.targetchecks miss clicks on child elements.
Best Practices
- Use delegation for lists/collections: More efficient, works with dynamic content.
- Use
closest()for delegation: Handles clicks on nested elements. - Avoid
stopPropagationunless necessary: Can cause unexpected issues. - Prefer delegation over many listeners: Better performance, less memory.
Summary
Events bubble up from target to ancestors by default. Event delegation attaches one listener to a parent to handle events on children. Use e.target.closest(selector) for robust delegation. stopPropagation() stops bubbling but use sparingly. Delegation is essential for dynamic content and better performance.
Code Examples
// Three phases:
// 1. Capture: window → document → ... → parent → target
// 2. Target: event fires on target
// 3. Bubble: target → parent → ... → document → window
document.body.addEventListener('click', (e) => {
console.log('Body clicked (bubble phase)');
});
document.body.addEventListener('click', (e) => {
console.log('Body clicked (capture phase)');
}, true); // or { capture: true }child.addEventListener('click', (e) => {
e.stopPropagation(); // Parent won't see this event
console.log('Child clicked');
});
// stopImmediatePropagation stops other listeners on SAME element too
child.addEventListener('click', (e) => {
e.stopImmediatePropagation();
});
child.addEventListener('click', (e) => {
// This won't run!
});