Skip to main content

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.target is where the event happened, event.currentTarget is 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, mouseenter and mouseleave do not.

Quick example

html
<div id="parent"> <button id="child">Click me</button> </div>
javascript
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:

text
Button Parent BODY

The handler on the clicked element ran first, then the event bubbled up the tree all the way to body.

The mechanism of bubbling

  1. The event starts on a specific element, that is event.target.
  2. Then it is passed upward: target -> its parent -> ... -> document -> window.
  3. At every level the handlers, if any, are invoked.

Visually the path looks like this:

text
<button> -> <div> -> <body> -> <html> -> document -> window

Every 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 }.

javascript
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.

html
<ul id="menu"> <li>Home</li> <li>Catalogue</li> <li>Contacts</li> </ul>
javascript
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:

javascript
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:

javascript
event.stopPropagation();
javascript
child.addEventListener('click', (e) => { console.log('Button'); e.stopPropagation(); // bubbling stopped });

Now the console shows only:

text
Button

Note 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:

  • blur and focus
  • mouseenter and mouseleave

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

ConceptDescription
Event bubblingThe process by which an event rises from a nested element to its parents
event.targetThe element where the event happened
event.currentTargetThe element whose handler is running right now
event.stopPropagation()Stops the bubbling
Used forDelegation, global handlers, logging

Common mistakes

  • Confusing target with currentTarget. In delegation you must check e.target (the real target), while this and currentTarget always 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 on document, and the bug surfaces far away from the call.
  • Expecting focus or mouseenter to bubble. They do not; for delegation use focusin or mouseover.
  • 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.