Event bubbling

Also known as: event propagation

On this page 7

Event bubbling is the phase of DOM event dispatch in which an event, after reaching its target element, travels upward through every ancestor to the document and finally the window, triggering matching listeners at each level. In browser JavaScript it is the default propagation behavior for most user interface events, and it is the reason a click on a button inside a list item can be handled by a listener attached to the list itself.

Bubbling is often called event propagation, although strictly speaking propagation covers three phases: capturing, target and bubbling. Understanding the bubbling phase is essential for event delegation, for avoiding duplicate handlers and for controlling how nested components react to the same user action.

How event bubbling moves through the DOM tree

Event bubbling works by computing a propagation path at the moment of dispatch and then walking that path in reverse. When the browser dispatches an event, it records the chain from the window down to the target: window, document, the html element, body and every ancestor of the target. This path is fixed before any listener runs, so removing or moving an element during handling does not change which ancestors still receive the event.

Dispatch then runs in three ordered phases. The capturing phase goes from the window down to the parent of the target, the target phase runs listeners on the target itself, and the bubbling phase climbs back up to the window. A listener registered with addEventListener(type, handler) runs in the bubbling phase by default; passing { capture: true } moves it to the capturing phase.

PhaseDirectioneventPhase valueListeners that run
CapturingWindow down to the target’s parent1Listeners registered with capture set to true
TargetAt the target element2All listeners on the target, in registration order
BubblingTarget’s parent up to the window3Listeners registered without capture
The three phases of DOM event dispatch

Inside a handler, two properties describe the position of the event. event.target is the element where the event originated, while event.currentTarget is the element whose listener is currently running. During bubbling these differ on every ancestor, and confusing them is one of the most common sources of bugs in delegated handlers.

Which events bubble and which do not

Not every event bubbles, and the event.bubbles property reports the behavior for each dispatched event. Most pointer, keyboard and input events bubble, because they describe interaction that ancestors may reasonably care about. Events that describe a state change of one specific element, such as loading or gaining focus, are often non-bubbling by design.

EventBubblesBubbling alternative or note
click, input, keydownYesStandard choice for delegation
focus, blurNoUse focusin and focusout, which bubble
mouseenter, mouseleaveNoUse mouseover and mouseout, which bubble
load, error on imagesNoUse a capturing listener on an ancestor
scroll on an elementNoScroll on the document does reach the window
submit, changeYesBubble in all current browsers
Bubbling behavior of common event types

Custom events created with new CustomEvent(type, init) do not bubble unless the init object sets bubbles: true. Developers who dispatch custom events from components frequently forget this flag and then wonder why an ancestor listener never fires. Events crossing a shadow DOM boundary also need composed: true to leave the shadow root, and their target is retargeted to the host element when observed from outside.

Browser JavaScript offers three methods that change what happens after a listener runs, and each targets a different concern. Two of them stop the upward travel of the event, while the third only cancels the browser’s default action and leaves propagation untouched.

MethodEffect on other listenersEffect on default action
stopPropagation()Ancestors no longer receive the event; other listeners on the current element still runNone
stopImmediatePropagation()Remaining listeners on the current element and all ancestors are skippedNone
preventDefault()None; the event keeps bubblingCancels the default action if the event is cancelable
Methods that influence propagation and default behavior

The distinction matters in practice. Calling preventDefault() on a link click prevents navigation but still lets analytics listeners higher in the tree observe the click. Calling stopPropagation() instead hides the click from those listeners but does not prevent navigation. Mixing them up produces either broken tracking or unexpected page loads.

Passive listeners, registered with { passive: true }, promise never to call preventDefault(). Browsers use that promise to keep scrolling smooth on touch and wheel events, which is why passive mode is the default for those events on the window, document and body in modern engines.

Event bubbling compared with capturing and delegation

Event bubbling, event capturing and event delegation are related but distinct ideas. Capturing is the opposite direction of travel, from the root down; delegation is a design pattern that relies on bubbling to handle events for many children with a single ancestor listener. A plain event listener on the target uses neither pattern deliberately but still participates in both phases of the dispatch.

ConceptWhat it isTypical use
Event bubblingUpward phase of dispatchDefault behavior for most handlers
Event capturingDownward phase of dispatchIntercepting events before children see them
Event delegationPattern built on bubblingOne listener for many or dynamic children
Event listenerFunction registered for an event typeReacting to an event at any phase
How bubbling relates to neighboring concepts

Capturing is the right tool when an ancestor must act before any descendant, for example to close an open menu on any outside click or to log interactions that child code might stop. Bubbling is the right tool when the ancestor should react after the target has had its chance, which is the usual expectation in component trees.

Practical patterns and common mistakes

The most valuable use of bubbling is event delegation for dynamic content. A table with thousands of rows, or a list that changes after network requests, can use one listener on the container and identify the relevant row with event.target.closest(selector). This keeps memory use and setup cost low and avoids reattaching listeners after every update.

  • Overusing stopPropagation breaks unrelated code higher in the tree, including outside-click handlers, analytics and focus management in dialogs.
  • Relying on event.target alone fails when the click lands on a nested icon or span; use closest() to find the intended element.
  • Forgetting bubbles: true on custom events leaves ancestor listeners silent.
  • Assuming focus bubbles leads to missing handlers; listen for focusin and focusout instead.
  • Handling the same click twice happens when both a child and an ancestor react to it without checking which one should own the action.

Frameworks add their own layer. Some UI libraries attach a single root listener and simulate bubbling through their component tree, so a native stopPropagation() call inside a third-party widget may or may not stop a framework handler depending on the order of attachment. When behavior seems inconsistent, inspecting event.eventPhase and event.currentTarget in the browser debugger usually reveals the actual dispatch order.

The idea in brief

Imagine a set of boxes nested inside each other. When you tap the smallest box, the tap is first noticed by that box, then by the box around it, then by the next one, all the way to the outermost box. Each box gets a chance to respond. That upward chain of responses is event bubbling.

It matters because it lets a web page respond to many items with one rule. Instead of teaching every button in a long list what to do, the page can teach the list once and let it work out which button was pressed. The key idea is simple: an action on an inner element is also an action on its containers, unless the page deliberately stops it.

A practical scenario

A team builds an order management screen that shows about 2,000 order rows, each with Edit, Duplicate and Cancel buttons. The first version attaches three click listeners to every row, which means roughly 6,000 listeners that must be recreated whenever the table refreshes after filtering. On mid-range laptops the refresh visibly stutters.

The developers replace them with one click listener on the table body. The handler calls event.target.closest('button[data-action]'), reads the action name and the row identifier from data attributes, and ignores clicks that do not land on such a button. A separate modal listens on the document in the capturing phase to close itself on outside clicks, so it no longer depends on rows avoiding stopPropagation().

The result is a single listener that survives every refresh, simpler code and faster updates, and newly loaded rows work immediately without any extra setup.