Event Bubbling and Capturing in JavaScript
This article provides a clear overview of the JavaScript event propagation model, focusing specifically on event capturing and event bubbling. You will learn the three phases of the DOM event flow, how events travel between nested HTML elements, how to configure event listeners for capturing or bubbling, and how to stop propagation when necessary.
The Three Phases of Event Flow
When an event occurs in a web browser—such as a click on a button
inside nested div containers—the browser executes the event
in three distinct phases defined by the standard DOM Level 2 Event
Model:
- Capturing Phase (Trickling): The event starts at
the
windowobject and travels downward through the DOM tree until it reaches the target element. - Target Phase: The event reaches the actual element
that initiated the interaction (the
event.target). - Bubbling Phase: The event travels back up the DOM
tree from the target element to the
windowobject.
Window -> Document -> <html> -> <body> -> Parent -> [ Target Element ] (Capturing Phase)
|
Target Phase
|
Window <- Document <- <html> <- <body> <- Parent <- [ Target Element ] (Bubbling Phase)
What is Event Bubbling?
Event bubbling is the default behavior in modern JavaScript. When an
event fires on an inner child element, it automatically triggers the
same event on its parent elements, continuing upward all the way to
document and window.
Example of Event Bubbling
Consider the following HTML structure:
<div id="parent">
<button id="child">Click Me</button>
</div>document.getElementById('parent').addEventListener('click', () => {
console.log('Parent clicked');
});
document.getElementById('child').addEventListener('click', () => {
console.log('Child clicked');
});When you click the button (#child), the console outputs:
1. Child clicked 2. Parent clicked
Because bubbling moves bottom-up, the child listener fires first, followed by the parent listener.
What is Event Capturing?
Event capturing (also referred to as trickling) is the reverse of bubbling. In this phase, the event starts at the topmost ancestor and moves downward toward the target element.
By default, event listeners do not respond to the capturing phase. To
execute a handler during capturing, pass true or
{ capture: true } as the third parameter to
addEventListener.
Example of Event Capturing
document.getElementById('parent').addEventListener('click', () => {
console.log('Parent clicked (Capture)');
}, true);
document.getElementById('child').addEventListener('click', () => {
console.log('Child clicked');
});When you click the button, the console outputs: 1.
Parent clicked (Capture) 2. Child clicked
The parent intercepts the event before it reaches the child.
Stopping Event Propagation
You can prevent an event from continuing through the capturing or
bubbling phases by calling event.stopPropagation() inside
your handler.
document.getElementById('child').addEventListener('click', (event) => {
event.stopPropagation();
console.log('Child clicked, propagation stopped');
});If multiple listeners are attached to the exact same element for the
same event type, event.stopImmediatePropagation() will stop
propagation to parent elements as well as prevent any remaining
listeners on the current element from executing.
Why Bubbling Matters: Event Delegation
Understanding bubbling enables event delegation, a pattern where you place a single event listener on a parent element instead of attaching individual listeners to multiple child elements.
document.getElementById('parent-list').addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
console.log('List item clicked:', event.target.textContent);
}
});Because child click events bubble up to #parent-list,
the single listener handles all present and dynamically added
<li> elements, improving memory efficiency and
performance.