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:
- First on the
<button>itself. - Then on the surrounding
<article>. - Then on the
<section>. - And onward, all the way up to the
documentobject.
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:
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.targetis where the event originated. It stays fixed for the entire trip up the tree.this(equal toevent.currentTarget) is the element whose handler is running right now. It changes at each step of bubbling.
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(andevent.currentTarget) is the<form>, because that is where the handler lives.event.targetis the specific element the user actually clicked.
See it live:
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:
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:
- Capturing — the event descends from the root toward the target.
- Target — the event arrives at the target element.
- 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.
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:
HTML→BODY→SECTION→ARTICLE→BUTTON— capturing phase, the handlers registered withtrue.BUTTON→ARTICLE→SECTION→BODY→HTML— 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 ↓ HTML→ BODY→ SECTION→ ARTICLE→ BUTTON
bubble ↑ BUTTON→ ARTICLE→ SECTION→ BODY→ HTML
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:
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 withaddEventListener(..., true)(or{capture: true}). - At the target: handlers on the target itself run.
- Up: from
event.targetback to the root, running bubbling handlers — those set viaon<event>, HTML attributes, oraddEventListenerwith no third argument (orfalse/{capture: false}).
Every handler can read these properties off the event:
event.target— the deepest element that originated the event.event.currentTarget(equal tothis) — 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.