Masterbook/Chapter 44• 4 min read
View Raw Code on GitHub

What is event bubbling and capturing?

When you click, type or otherwise interact with elements on a page, the browser fires an "event" that moves through the document tree. How and where you respond to that event depends on understanding the two propagation phases:

Capturing (also called trickling) - The event is dispatched from the top of the DOM hierarchy (window or document) and moves downward through each ancestor element until it reaches the actual target. Listeners registered with addEventListener() using {capture: true} fire in this phase, starting from the outermost ancestor and ending with the target. Capturing is disabled by default because most code relies on bubbling, but it's available when you need to intercept an event before it hits its destination.

Target phase - Once the event reaches the element that originally triggered it (for example, the button you clicked), any event listeners attached directly to that element run, regardless of whether they were registered for capturing or bubbling.

Bubbling - After the target has processed the event, it bubbles back up the DOM tree. The event
moves from the target's parent up to the root, invoking listeners along the way. This is the default behavior for most event types. When you call addEventListener() without options, you're registering a listener that will fire during this bubbling phase.

Understanding these phases allows you to decide where to attach your handlers:

Normal use case (bubbling): Attach a listener on a parent element to handle events from many
children (a technique known as event delegation). For example, a click on a list item bubbles up to the

    , and a click handler on the
      can inspect event.target to determine which
    • was clicked.

      Capturing use case: Attach a listener with {capture: true} when you need to act before the event reaches the target. This can be useful for intercepting keyboard shortcuts or gestures at the document level or for implementing custom UI controls that need to block events from hitting certain elements.

      Several other points are important for both beginners and experienced developers:

      You can stop an event from continuing along its propagation path by calling event stopPropagation(). To prevent other handlers on the same element from running as well, call
      event.stopImmediatePropagation().

      The properties event.target and event.currentTarget differ: event.target is the element where the event originated, while event.currentTarget is the element whose listener is currently executing.

      Not all events bubble (e.g. focus and blur do not), and some behave inconsistently across older
      browsers, so always test your code in the environments you support.

      By controlling where in the capture/bubble phases your handlers run and understanding how the
      event travels, you can build more efficient and responsive UIs, whether you're just starting out or writing complex component systems.

      Bubbling in action

      Consider Nested Elements:

      <div id="outer">
          <button id="inner">Click me</button>
      </div>
      

      If you add click handlers on both elements:

      document.getElementById("outer").addEventListener("click", () => console.log("outer"));
      document.getElementById("inner").addEventListener("click", () => console.log("inner"));
      

      Clicking the button logs:
      inner
      outer

      The click originates at #inner, triggers its handler, then bubbles up to #outer and runs the outer handler.

      Capturing

      Capturing is the mirror image of bubbling. The event starts at the top of the tree and flows down to the target, invoking handlers registered for the capture phase. To attach a listener during capture, provide { capture: true }:

      document.getElementById("outer").addEventListener("click", () => console.log("outer capture"),{capture: true});
      document.getElementById("inner").addEventListener("click", () => console.log("inner"));
      

      Now clicking the button logs:

      outer capture
      inner

      The capturing handler runs before the target and bubbling handlers because the event travels down the tree first.

      Controlling propagation

      Inside an event handler you can stop the event from continuing along the propagation path:

      • event.stopPropagation() halts further propagation through both capturing and bubbling phases.
      • event.stopImmediatePropagation() additionally prevents other handlers on the same element from running.

      Stopping propagation is useful when you don't want parent elements to receive the event (for example, clicking inside a modal dialog should not close the underlying page).

      Real-world analogy

      Imagine dropping a stone into a pond. The initial splash is the target phase. Ripples that travel outward to the shore resemble bubbling: the disturbance spreads from the point of impact to the edges. Now imagine someone at the shore sending a vibration back toward the point where the stone hit — that's capturing: energy travels from the outside in.

      Summary

      • DOM events have three phases: capturing (from root down), target, and bubbling (from target up).
      • Most event listeners fire during bubbling by default. Pass { capture: true } to listen during the capturing phase.
      • stopPropagation() and stopImmediatePropagation() let you prevent an event from continuing along its path.
      • Understanding propagation helps with advanced patterns like event delegation and preventing unwanted side effects.

      Common misconceptions

      1. "Events only bubble." Many events bubble by default, but capturing exists and is enabled by passing { capture: true }.
      2. "event.target equals event.currentTarget." event.target is the element where the event originated; event.currentTarget is the element whose listener is currently executing. They differ when handling bubbling events on ancestors.
      3. "Bubbling can't be stopped." Calling event.stopPropagation() halts the event's travel up (and down) the DOM tree.