Bubbling and capturing

Put a click handler on an <aside>, then click a button buried inside it. The handler still fires. That surprises people the first time, and once you see why, a whole family of patterns opens up.

Here is the setup that raises the question:

<aside onclick="alert('The handler!')">
  <button>If you click on <code>BUTTON</code>, the handler on <code>ASIDE</code> runs.</button>
</aside>

You clicked the <button>, not the <aside>. So why did the <aside> handler run? The answer is a mechanism called bubbling, and it is one of the load-bearing ideas in browser events.

Bubbling

The rule is short:

When an event fires on an element, the browser runs that element’s handlers first, then its parent’s, then its grandparent’s, and so on up the tree.

Take three nested elements, SECTION > ARTICLE > BUTTON, each carrying its own click handler:

<style>
  body * {
    margin: 10px;
    border: 1px solid teal;
  }
</style>

<section onclick="alert('section')">SECTION
  <article onclick="alert('article')">ARTICLE
    <button onclick="alert('button')">BUTTON</button>
  </article>
</section>

Click the inner <button> and the browser walks upward, firing each onclick it finds:

  1. First on the <button> itself.
  2. Then on the surrounding <article>.
  3. Then on the <section>.
  4. And onward, all the way up to the document object.
SECTIONARTICLEBUTTONclick (target)1 → 22 → 3alerts fire: button → article → section
A click on BUTTON fires handlers from the inside out. The event starts at the target and rises through each ancestor.

So a click on <button> produces three alerts in sequence: button, then article, then section. The event “bubbles” outward from the innermost element through each parent, the way a bubble rises through water. That image is where the name comes from.

Rather than three alerts, this version logs the order into a panel. Click BUTTON, then ARTICLE, then SECTION and watch how far the event climbs from each starting point:

interactiveWatch a click bubble up the tree

event.target

Since one parent handler can catch clicks from anywhere below it, that handler needs a way to know what was actually clicked. That is event.target.

The deepest element that started the event is the target, available as event.target.

It is easy to confuse this with this, so hold the two apart:

  • event.target is where the event originated. It stays fixed for the entire trip up the tree.
  • this (equal to event.currentTarget) is the element whose handler is running right now. It changes at each step of bubbling.
on BUTTON handler
this → BUTTON
event.target → BUTTON
on ARTICLE handler
this → ARTICLE
event.target → BUTTON
on SECTION handler
this → SECTION
event.target → BUTTON
During bubbling, event.target stays pinned to the clicked BUTTON, while this / currentTarget points at whichever handler is executing.

This split is what makes a single form.onclick so useful. No matter which button, label, or icon inside the form gets clicked, the event bubbles up to <form> and the one handler runs. Inside it:

  • this (and event.currentTarget) is the <form>, because that is where the handler lives.
  • event.target is the specific element the user actually clicked.

See it live:

interactiveOne form handler, many targets

One edge case worth noting: event.target can equal this. That happens when the click lands directly on the <form> itself — say, on its padding — rather than on a child.

Stopping bubbling

By default the event rides all the way up: past <html>, into document, and for some events onward to window, firing every handler it passes.

A handler can cut that short. If it decides the event is fully dealt with, it calls event.stopPropagation() and the upward journey ends there.

Here, clicking the button never reaches section.onclick:

<section onclick="alert('the bubbling never reaches here')">
  <button onclick="event.stopPropagation()">Click me</button>
</section>

Toggle the checkbox to switch stopPropagation() on and off, then click the button. With it on, the outer panel’s handler never learns about the click:

interactivestopPropagation, on and off

Capturing

Bubbling is only half the story. Before an event bubbles up, it first travels down. That downward leg is the capturing phase. You will rarely reach for it, but knowing it exists completes the mental model.

The DOM Events specification defines three phases:

  1. Capturing — the event descends from the root toward the target.
  2. Target — the event arrives at the target element.
  3. Bubbling — the event climbs back up from the target to the root.

Here is the standard diagram for a click on a <button> inside the section: capturing (1) going down the left, target (2) at the bottom, bubbling (3) going back up the right.

documentHTMLSECTIONARTICLEBUTTON (target)1 capture3 bubble2 target
The full round trip: an event descends through ancestors (capturing), reaches the target, then rises back through the same ancestors (bubbling).

So for a click on <button>: the event first threads down through the ancestor chain (capturing), reaches the target and fires there (target), then travels back up (bubbling), running handlers along the way.

Everything so far has been about bubbling for a reason: capturing is almost never used. It has also been invisible up to now. Handlers you set with an on<event> property, an HTML attribute, or a plain two-argument addEventListener(event, handler) are all bubbling-phase handlers — they know nothing about capturing and only fire on phases 2 and 3.

To catch the capturing phase, pass the capture option as true:

elem.addEventListener(..., {capture: true})

// "true" on its own is shorthand for {capture: true}
elem.addEventListener(..., true)

The capture option has two settings:

  • false (the default) — the handler runs on the bubbling phase.
  • true — the handler runs on the capturing phase.

There are formally three phases, but the target phase is not handled on its own. Handlers registered for either capturing or bubbling both fire when the event reaches the target.

Here is capturing and bubbling side by side:

<style>
  body * {
    margin: 10px;
    border: 1px solid teal;
  }
</style>

<section>SECTION
  <article>ARTICLE
    <button>BUTTON</button>
  </article>
</section>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

This attaches two click handlers — one capturing, one bubbling — to every element on the page, so you can watch which fires when.

Click on <button> and the order is:

  1. HTMLBODYSECTIONARTICLEBUTTON — capturing phase, the handlers registered with true.
  2. BUTTONARTICLESECTIONBODYHTML — bubbling phase, the handlers registered without it.

Notice BUTTON appears twice. It sits at the boundary of both phases: it is the last stop of capturing and the first stop of bubbling, so both of its handlers run there.

capture ↓ HTMLBODYSECTIONARTICLEBUTTON

bubble ↑ BUTTONARTICLESECTIONBODYHTML

Full path for a click on BUTTON: down through capturing handlers, then up through bubbling handlers. The target BUTTON runs both.

Instead of a burst of alerts, this demo logs each handler as it fires. Click P and read the log top to bottom: five capturing lines coming down, then five bubbling lines going back up, with P on the boundary:

interactiveCapturing then bubbling, in order

There is a property event.eventPhase that reports which phase caught the event as a number. It is rarely needed, since you generally already know from the handler you wrote.

Summary

When an event happens, the deepest element involved becomes the target (event.target). From there the browser runs a three-part journey:

  • Down: from the document root to event.target, running any capturing handlers — the ones added with addEventListener(..., true) (or {capture: true}).
  • At the target: handlers on the target itself run.
  • Up: from event.target back to the root, running bubbling handlers — those set via on<event>, HTML attributes, or addEventListener with no third argument (or false / {capture: false}).

Every handler can read these properties off the event:

  • event.target — the deepest element that originated the event.
  • event.currentTarget (equal to this) — the element currently handling the event.
  • event.eventPhase — the current phase (capturing = 1, target = 2, bubbling = 3).

Any handler may call event.stopPropagation() to end the trip early, but reach for it sparingly — you can rarely be certain nothing higher up will want the event, possibly for something unrelated.

Capturing is used seldom; almost all real code handles events on the bubbling phase, and there is a sensible reason for that. Think of how emergencies get handled: local responders act first because they know the scene best, and only then do higher authorities step in if needed. Handlers work the same way. The code that put a handler on a specific <td> knows the most about that cell and its job, so it deserves the first shot. Its parent knows the broader context but less detail, and so on up to the top-level handlers that deal in general concerns and run last.

Bubbling and capturing are the groundwork for event delegation — a compact, powerful pattern we take up in the next chapter.