Event bubbling
Event bubbling is the mechanism by which an event that happened on a nested element is passed up the DOM tree to its parents.
In other words: when you click a button, the browser first handles the event on the button itself, then lifts it to the container, to body and even to document.
Theory
TL;DR
- The event starts on a specific element (
event.target) and then travels up the chain of ancestors. - At every level the handlers attached to that element run.
event.targetis where the event happened,event.currentTargetis whose handler is running right now.- Bubbling enables event delegation: one listener on the parent serves any number of children, including ones added dynamically.
event.stopPropagation()is what stops the upward travel.- Not every event bubbles:
focus,blur,mouseenterandmouseleavedo not.
Quick example
<div id="parent">
<button id="child">Click me</button>
</div>document.querySelector('#child').addEventListener('click', () => {
console.log('Button');
});
document.querySelector('#parent').addEventListener('click', () => {
console.log('Parent');
});
document.body.addEventListener('click', () => {
console.log('BODY');
});A click on the #child button logs:
Button
Parent
BODYThe handler on the clicked element ran first, then the event bubbled up the tree all the way to body.
The mechanism of bubbling
- The event starts on a specific element, that is
event.target. - Then it is passed upward:
target-> its parent -> ... ->document->window. - At every level the handlers, if any, are invoked.
Visually the path looks like this:
<button> -> <div> -> <body> -> <html> -> document -> windowEvery level can "hear" the event as long as it has a handler.
Formally, bubbling is the third phase of event propagation. The full cycle is: capturing from window down to the target, then the target phase, then bubbling back up. A plain addEventListener(type, handler) listens on the bubbling phase; to listen during capturing you pass { capture: true }.
parent.addEventListener('click', (e) => {
console.log(e.target.id); // child: where the click happened
console.log(e.currentTarget.id); // parent: whose handler is running
});Why bubbling is useful
1. Event delegation (fewer handlers). You can attach one handler to a parent element and catch events from all of its children.
<ul id="menu">
<li>Home</li>
<li>Catalogue</li>
<li>Contacts</li>
</ul>menu.addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log('Item clicked:', e.target.textContent);
}
});This works even for new <li> elements added dynamically, because the listener sits on the list, not on each item. Without bubbling the approach would be impossible.
2. Global listeners. You can listen on the whole document, for example to close a dropdown on an outside click or to log activity:
document.addEventListener('click', (e) => {
console.log('Click on the page');
});Stopping the bubbling
Sometimes you do not want the event to travel any further. In that case you use:
event.stopPropagation();child.addEventListener('click', (e) => {
console.log('Button');
e.stopPropagation(); // bubbling stopped
});Now the console shows only:
ButtonNote that stopPropagation() does not cancel the browser's default action, that is what preventDefault() is for.
What does not bubble
Some events do not bubble, for example:
blurandfocusmouseenterandmouseleave
They have bubbling counterparts: focusin and focusout instead of focus and blur, and mouseover and mouseout instead of mouseenter and mouseleave. You can check any particular event through the event.bubbles property.
Summary table
| Concept | Description |
|---|---|
| Event bubbling | The process by which an event rises from a nested element to its parents |
| event.target | The element where the event happened |
| event.currentTarget | The element whose handler is running right now |
| event.stopPropagation() | Stops the bubbling |
| Used for | Delegation, global handlers, logging |
Common mistakes
- Confusing
targetwithcurrentTarget. In delegation you must checke.target(the real target), whilethisandcurrentTargetalways point at the element that holds the listener. - Checking only
e.target.tagName. If the<li>contains a<span>or an icon, that child becomes the target.e.target.closest('li')is the reliable version. - Adding
stopPropagation()just in case. It breaks delegation and global listeners ondocument, and the bug surfaces far away from the call. - Expecting
focusormouseenterto bubble. They do not; for delegation usefocusinormouseover. - Assuming bubbling is the only phase. A listener with
{ capture: true }fires before every bubbling handler, including the one on the target itself.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.