Skip to content
Plurafy

Event Bubbling and Delegation

Understanding event propagation in the DOM

Efficiently handle events on dynamic elements and understand event flow.

Events propagate through the DOM in phases: capture, target, and bubbling. Event delegation leverages this to handle many elements with few listeners.

// Three phases of event propagation
element.addEventListener('click', handler, true); // Capture phase
element.addEventListener('click', handler, false); // Bubbling phase (default)
// Phase 1: Capture (top-down)
// Phase 2: Target (the clicked element)
// Phase 3: Bubble (bottom-up)
// Document
// ↓ (capture)
// HTML
// ↓
// Body
// ↓
// Div
// ↓
// Button ← Clicked
// ↑ (bubble)
// Div
// ↑
// Body
// ↑
// HTML
// ↑
// Document
const parent = document.getElementById('parent');
const child = document.getElementById('child');
// Bubbling: child event triggers parent listeners
child.addEventListener('click', () => {
console.log('Child clicked');
});
parent.addEventListener('click', () => {
console.log('Parent clicked (via bubbling)');
});
// Clicking child logs:
// 'Child clicked'
// 'Parent clicked (via bubbling)'
// Stop propagation
child.addEventListener('click', (event) => {
event.stopPropagation(); // Prevents bubbling
console.log('Child clicked');
});
// Now parent listener won't be called
// ❌ Inefficient: Add listener to each item
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', () => {
item.classList.add('active');
});
});
// ✅ Efficient: Single listener on parent
const list = document.getElementById('list');
list.addEventListener('click', (event) => {
if (event.target.matches('.item')) {
event.target.classList.add('active');
}
});
// Works for dynamically added items too
const newItem = document.createElement('div');
newItem.className = 'item';
list.appendChild(newItem); // Already has click handler via delegation
const link = document.querySelector('a');
// preventDefault: Cancel default action
link.addEventListener('click', (event) => {
event.preventDefault(); // Link won't navigate
});
// stopPropagation: Stop bubbling
const nested = document.getElementById('nested');
nested.addEventListener('click', (event) => {
event.stopPropagation(); // Won't bubble to parent
console.log('Nested clicked');
});
// stopImmediatePropagation: Stop all handlers
element.addEventListener('click', (event) => {
event.stopImmediatePropagation();
console.log('First');
});
element.addEventListener('click', () => {
console.log('Second'); // Won't execute
});
element.addEventListener('click', (event) => {
// Properties
event.target; // Element that triggered event
event.currentTarget; // Element with listener
event.type; // 'click'
event.timestamp; // When event occurred
// Methods
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
// Mouse events
event.clientX, event.clientY; // Viewport coordinates
event.pageX, event.pageY; // Page coordinates
event.screenX, event.screenY; // Screen coordinates
event.button; // 0=left, 1=middle, 2=right
// Keyboard events
event.key; // 'Enter', 'Shift', etc
event.code; // 'KeyA', 'ShiftLeft', etc
event.ctrlKey; // Modifier keys
event.shiftKey;
event.altKey;
event.metaKey;
});
const input = document.querySelector('input');
// Focus: When element gets focus
input.addEventListener('focus', () => {
input.classList.add('focused');
});
// Blur: When element loses focus
input.addEventListener('blur', () => {
input.classList.remove('focused');
});
// focusin/focusout: Bubble versions
input.addEventListener('focusin', () => {
// Can be caught on parent
});
// Rich form with dynamic elements
const form = document.getElementById('dynamic-form');
form.addEventListener('click', (event) => {
const btn = event.target.closest('button[data-action]');
if (!btn) return;
const action = btn.dataset.action;
switch(action) {
case 'add':
addField();
break;
case 'remove':
event.target.closest('.field').remove();
break;
case 'submit':
event.preventDefault();
submitForm();
break;
}
});
function addField() {
const field = document.createElement('input');
field.type = 'text';
form.appendChild(field);
}