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

> Master DOM event bubbling and capturing. Understand event propagation phases and leverage event delegation for efficient single-listener event handling.

- Repository: [Leonardo Maldonado/33-js-concepts](https://github.com/leonardomso/33-js-concepts)
- Tags: deep-dive
- Published: 2026-03-04

---

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

```html
<div class="grandparent">
  <div class="parent">
    <button class="child">Click me</button>
  </div>
</div>

```

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

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

```javascript
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()`:

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