Event Bubbling and Capturing — Complete Guide
In this tutorial, you will learn about Event Bubbling and Capturing. We cover key concepts, practical examples, and best practices to help you master this topic.
Event propagation has three phases — capture (root to target), target, and bubble (target to root) — controlling how events travel through the DOM tree and trigger listeners on ancestor elements.
What You'll Learn
- The three phases of event propagation: capture, target, bubble
- How to use stopPropagation and stopImmediatePropagation
- When bubbling is useful and when it causes problems
- How to determine which phase a listener fires in
Why It Matters
Without understanding propagation, you will encounter mysterious bugs: clicks on a child element triggering parent handlers, form submit handlers firing twice, or event delegation not working as expected.
Real-World Use
- A modal overlay closes when clicking outside the modal content (bubbling to the overlay)
- A table uses event delegation on the tbody to handle clicks on any row
- A nested menu structure uses stopPropagation to prevent parent menus from closing
flowchart TD
subgraph Capture Phase
A1[document] --> A2[html]
A2 --> A3[body]
A3 --> A4[parent]
A4 --> A5[target]
end
subgraph Bubble Phase
A5 --> B4[parent]
B4 --> B3[body]
B3 --> B2[html]
B2 --> B1[document]
end
Understanding Event Phases
Every DOM event follows a three-phase journey. The event object's eventPhase property indicates the current phase.
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');
// Capture phase listener (third argument true)
document.body.addEventListener('click', function(event) {
console.log('BODY capture phase:', event.eventPhase);
console.log('Phase value:', event.eventPhase === 1 ? 'CAPTURE' : '');
}, true);
// Target phase (fires on the clicked element)
child.addEventListener('click', function(event) {
console.log('CHILD target phase:', event.eventPhase);
console.log('Phase value:', event.eventPhase === 2 ? 'TARGET' : '');
});
// Bubble phase listener (default)
parent.addEventListener('click', function(event) {
console.log('PARENT bubble phase:', event.eventPhase);
console.log('Phase value:', event.eventPhase === 3 ? 'BUBBLE' : '');
});
Expected output: Clicking the child logs: body capture (phase 1), child target (phase 2), parent bubble (phase 3). The event travels document -> body -> child -> parent -> document.
Using stopPropagation
The stopPropagation method prevents the event from continuing to the next phase or to sibling listeners in the same phase.
const modal = document.querySelector('.modal');
const modalContent = document.querySelector('.modal-content');
const closeBtn = document.querySelector('.modal-close');
// Clicking the overlay closes the modal
modal.addEventListener('click', function() {
console.log('Overlay clicked — closing');
this.style.display = 'none';
});
// But clicking inside the modal content should NOT close it
modalContent.addEventListener('click', function(event) {
event.stopPropagation();
console.log('Content clicked — propagation stopped');
});
// Clicking the close button also stops propagation
closeBtn.addEventListener('click', function(event) {
event.stopPropagation();
console.log('Close button clicked');
modal.style.display = 'none';
});
Expected output: Clicking the overlay closes the modal. Clicking the modal content does nothing to the overlay because propagation is stopped at the content level. The close button also stops propagation.
stopImmediatePropagation
Unlike stopPropagation, stopImmediatePropagation also prevents other listeners on the same element from firing.
const button = document.querySelector('.emergency-btn');
// First handler
button.addEventListener('click', function(event) {
console.log('Handler 1');
});
// Second handler stops immediate propagation
button.addEventListener('click', function(event) {
console.log('Handler 2');
event.stopImmediatePropagation();
console.log('Stopped — subsequent handlers will not fire');
});
// Third handler (never runs)
button.addEventListener('click', function(event) {
console.log('Handler 3 — will NOT execute');
});
// With stopPropagation, Handler 3 would still run
// With stopImmediatePropagation, Handler 3 is skipped
Expected output: Clicking the button shows "Handler 1", "Handler 2", and the stop message. Handler 3 never executes because stopImmediatePropagation prevents it.
Event Delegation
Event delegation leverages bubbling to handle events on many elements with a single listener on a common ancestor.
// Without delegation: one listener per item
const listItems = document.querySelectorAll('.list-item');
listItems.forEach(item => {
item.addEventListener('click', function() {
console.log('Clicked:', this.textContent);
});
});
// With delegation: single listener on parent
const list = document.querySelector('.item-list');
list.addEventListener('click', function(event) {
// Find the closest list item (in case click was on a child element)
const item = event.target.closest('.list-item');
if (item) {
console.log('Delegated click:', item.textContent);
// Highlight the clicked item
this.querySelectorAll('.list-item').forEach(i => {
i.classList.remove('active');
});
item.classList.add('active');
}
});
// Delegation handles items added dynamically after listener attachment
const newItem = document.createElement('li');
newItem.className = 'list-item';
newItem.textContent = 'Dynamically added';
list.appendChild(newItem);
// Clicking this new item works without attaching a new listener!
Expected output: Clicking any list item highlights it and logs its text. Dynamically added items also work because the delegate listener is on the parent, not the individual items.
Bubbling Events vs Non-Bubbling Events
Not all events bubble. Knowing which ones bubble helps you choose the right delegation Strategy.
// Bubbling events (can be delegated):
// click, mousedown, mouseup, keydown, keyup, submit, focusin, blur
// touchstart, touchend
// Non-bubbling events (cannot be delegated directly):
// focus, blur, load, unload, scroll, mouseenter, mouseleave
// Focus delegation workaround: use focusin/focusout which bubble
const form = document.querySelector('form');
// This does NOT work for dynamically added inputs:
// form.addEventListener('focus', handler); // focus does not bubble
// This works:
form.addEventListener('focusin', function(event) {
const input = event.target;
console.log('Input focused:', input.name);
input.classList.add('focused');
});
form.addEventListener('focusout', function(event) {
const input = event.target;
input.classList.remove('focused');
});
// Mouseenter delegation workaround:
// Use mouseover and check event.target
Expected output: Focusing any input (including dynamically added ones) logs its name and adds the focused class. Blurring removes it.
Common Mistakes
- Calling stopPropagation unnecessarily — Stopping propagation can break other features that rely on bubbling (analytics tracking, tooltip dismissals, global click handlers).
- Forgetting that not all events bubble — Trying to delegate focus, blur, scroll, or load events without checking if they bubble causes silent failures.
- Using event.target instead of event.currentTarget inside a delegate —
event.targetis the element that triggered the event.event.currentTargetis the element the listener is attached to (the delegate parent). - Accidentally stopping propagation in capture phase — Stopping propagation in the capture phase prevents the event from reaching the target at all.
- Assuming stopPropagation prevents default — stopPropagation only stops event travel. Use preventDefault to prevent the browser's default action (like following a link).
Practice Questions
- What are the three phases of event propagation? Capture (root to target), Target (at the element), Bubble (target back to root).
- What is the difference between stopPropagation and stopImmediatePropagation? stopPropagation prevents the event from moving to other elements. stopImmediatePropagation also prevents other listeners on the same element from firing.
- Why is event delegation more efficient than individual listeners? It uses a single listener instead of many, saving memory. It also handles dynamically added elements without reattaching listeners.
- Challenge: Implement a tree view component where clicking a node expands/collapses its children, clicking a child selects it, and clicking the root does nothing. Use a single delegated click listener on the tree container.
FAQ
Mini Project
Build a todo list app using event delegation. A single ul container has one click listener. Clicking a todo item toggles its completed class. Each item has a delete button (a span with class "delete") that removes the item. An input field and add button create new todos. All interaction is handled through the single delegated listener on the ul.
What's Next
Continue with Lesson 14: Event Delegation Patterns to explore advanced delegation patterns for complex UI components.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro