How Event Bubbling and Capturing Work in the DOM: A Complete Guide to Event Propagation

Event bubbling and capturing are the two directional phases of DOM event propagation where events travel down from the window to the target element (capturing) and back up to the document root (bubbling), enabling the event delegation pattern that lets you handle events efficiently with a single listener on a parent element.

Understanding event bubbling and capturing is essential for mastering DOM interactions and building performant web applications. According to the leonardomso/33-js-concepts repository, every DOM event flows through three distinct phases that determine when and where your event listeners execute. This guide explains the mechanics of event propagation based on the source files in docs/beyond/concepts/event-bubbling-capturing.mdx and docs/beyond/concepts/event-delegation.mdx, showing you how to leverage these concepts for cleaner code.

The Three Phases of Event Propagation

Every DOM event follows a predictable path through the document tree. You can inspect the current phase via event.eventPhase, which returns 1 for CAPTURING, 2 for AT_TARGET, or 3 for BUBBLING.

Capturing Phase (Downward Travel)

The capturing phase moves the event down from the window object through the document hierarchy to the target element. Listeners registered with { capture: true } (or the legacy true boolean flag) trigger during this downward journey. As documented in docs/beyond/concepts/event-bubbling-capturing.mdx, this phase runs before the event reaches its destination.

Target Phase (The Source Element)

When the event arrives at the element that actually triggered the action, it enters the target phase. During this phase, all listeners on the target element execute, regardless of whether they were registered for capturing or bubbling.

Bubbling Phase (Upward Travel)

Finally, the bubbling phase carries the event back up from the target to the window object. By default, addEventListener registers handlers for this phase, which is why parent elements can intercept events triggered by their children.

How Event Bubbling Works in Practice

When a child element receives an interaction, its listeners fire first during the target phase, then the event propagates upward through every ancestor. Consider this nested structure from the repository examples:

<div class="grandparent">
  <div class="parent">
    <button class="child">Click me</button>
  </div>
</div>
document.querySelector('.grandparent').addEventListener('click', () => console.log('Grandparent'));
document.querySelector('.parent').addEventListener('click', () => console.log('Parent'));
document.querySelector('.child').addEventListener('click', () => console.log('Child'));

Clicking the button outputs:


Child
Parent
Grandparent

This sequence demonstrates the default bubbling behavior where events start at the target and rise through the DOM tree, as implemented in the leonardomso/33-js-concepts source code.

Intercepting Events with the Capture Phase

To execute logic before an event reaches its target, register listeners with the capture flag. This pattern is useful for global logging or implementing "cancel-all-clicks" functionality.

document.querySelector('.parent').addEventListener('click', () => {
  console.log('Parent – capturing');
}, { capture: true });

With this configuration, "Parent – capturing" logs first, followed by the child's handler, then any bubbling listeners. The repository notes that capturing is rarely needed but provides specific use cases in docs/beyond/concepts/event-bubbling-capturing.mdx.

Event Delegation Pattern

Because events bubble, you can attach a single listener to a common ancestor and handle actions for all current and future child elements. This pattern reduces memory usage and eliminates the need to attach listeners to every individual element.

const container = document.querySelector('.button-container');

container.addEventListener('click', (e) => {
  if (e.target.matches('.btn')) {
    handleClick(e.target);
  }
});

The listener runs once per click regardless of how many .btn elements exist, and it automatically works for dynamically added buttons. The full delegation guide in docs/beyond/concepts/event-delegation.mdx expands on implementation details and pitfalls.

For more robust delegation that handles clicks on child elements inside the target, use Element.closest():

document.querySelector('.list').addEventListener('click', (e) => {
  const btn = e.target.closest('.item');
  if (btn) {
    console.log('Clicked:', btn.textContent);
  }
});

Controlling Event Propagation

Two methods allow you to stop event travel, though both should be used sparingly to avoid breaking delegated listeners like analytics tracking.

  • event.stopPropagation() prevents the event from reaching ancestor elements, but other listeners on the current element still execute.
  • event.stopImmediatePropagation() halts all remaining listeners on the current element, including those registered for the same phase.

The detailed comparison appears in docs/beyond/concepts/event-bubbling-capturing.mdx between lines 13-21.

Non-Bubbling Events and Workarounds

Not all events participate in the bubbling phase. Events like focus, blur, mouseenter, mouseleave, load, and scroll do not bubble. For delegation purposes, use their bubbling counterparts:

  • Use focusin and focusout instead of focus and blur
  • Use mouseover and mouseout instead of mouseenter and mouseleave

Summary

  • Events flow through three phases: capturing (down), target, and bubbling (up).
  • Bubbling is the default behavior that powers event delegation.
  • Capture listeners ({ capture: true }) execute on the way down the tree before reaching the target.
  • event.target refers to the element that triggered the event, while event.currentTarget refers to the element where the listener is attached—critical for delegation logic.
  • Stopping propagation with stopPropagation() or stopImmediatePropagation() should be limited to avoid breaking higher-level application logic.

Frequently Asked Questions

What is the difference between event.target and event.currentTarget?

event.target is the DOM element that actually triggered the event (the deepest element in the tree where the interaction occurred), while event.currentTarget is the element where the currently executing listener is attached. In delegation patterns, currentTarget is the parent container with the listener, and target (or target.closest()) identifies the specific child element clicked.

When should I use event capturing instead of bubbling?

Use the capturing phase when you need to intercept events before they reach their destination, such as for global analytics logging, implementing drag-and-drop overlays, or creating "click outside to close" handlers that must evaluate the entire path. According to the leonardomso/33-js-concepts repository, capturing is rarely needed for standard application development since bubbling handles most use cases.

How does event delegation improve performance?

Event delegation improves performance by reducing the number of attached listeners in memory. Instead of binding a listener to every button in a list (which consumes memory and requires cleanup), you bind one listener to the parent container. This pattern also automatically handles dynamically added elements without requiring new listener attachments, as explained in docs/beyond/concepts/event-delegation.mdx.

Which events do not bubble and how do I delegate them?

Events like focus, blur, mouseenter, mouseleave, load, and scroll do not bubble. To delegate focus-related actions, use focusin and focusout instead of focus and blur. For mouse enter/leave behaviors, use mouseover and mouseout, which do bubble and can be delegated through a parent container.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →