
Event Bubbling vs. Capturing in JavaScript

If you have ever clicked a button and watched several event handlers fire in places you did not expect, you have already met event propagation. The only question is whether you understood what the browser was doing at the time.
Beginners often learn that clicks can "bubble up" through the DOM, but that half‑explanation usually leaves out capturing, the target phase, and the reason some handlers run before others. That is where the confusion comes from. People remember one loose rule and then hit code that does not seem to follow it.
The browser is being consistent. It is just following a fuller model than most introductions bother to explain.
There are Three Phases to a DOM Event
When an event happens on an element, the browser does not simply fire one callback in one place and call it a day. The event moves through phases.
The useful mental model is:
- capturing phase: the event moves from outer ancestors down towards the target
- target phase: the event reaches the element where it actually happened
- bubbling phase: the event moves back up through ancestors
That is why a click on a button can end up triggering handlers on the button itself, its parent, its grandparent, and beyond.
If you only remember "events bubble", you miss the fact that the browser can also run handlers on the way down if you register them for the capturing phase.
Bubbling is the Default Most Developers See First
By default, addEventListener listens during the bubbling phase.
const panel = document.querySelector('.panel');const button = document.querySelector('.save-button');panel?.addEventListener('click', () => { console.log('Panel clicked');});button?.addEventListener('click', () => { console.log('Button clicked');});If the button is inside the panel and you click the button, the button handler runs and then the panel handler runs as the event bubbles back up.
That is the behaviour most people notice first because it is the default, and it is the basis for event delegation as well.
Capturing Runs First If You Ask for It
You can register a handler during the capturing phase by passing the appropriate option to addEventListener.
panel?.addEventListener( 'click', () => { console.log('Panel capture'); }, { capture: true },);Now the panel's capturing handler can run on the way down before the event reaches the button.
That means the browser can call handlers in an order like this:
- outer ancestor capture
- inner ancestor capture
- target
- inner ancestor bubble
- outer ancestor bubble
Once you understand that order, a lot of "why did that fire first?" moments stop being mysterious.
A Small Example Makes the Order Clearer
Imagine markup like this:
<div class="page"> <div class="panel"> <button class="save-button">Save</button> </div></div>and listeners like these:
const page = document.querySelector('.page');const panel = document.querySelector('.panel');const button = document.querySelector('.save-button');page?.addEventListener('click', () => { console.log('page bubble');});page?.addEventListener( 'click', () => { console.log('page capture'); }, { capture: true },);panel?.addEventListener('click', () => { console.log('panel bubble');});button?.addEventListener('click', () => { console.log('button target');});Clicking the button will usually log in this order:
page capturebutton targetpanel bubblepage bubble
That is not arbitrary. It reflects the event moving down during capture, reaching the target, then moving back up during bubbling.
event.target and event.currentTarget are not the same
This is another important point that becomes easier once you understand propagation.
event.target is the element where the event actually happened.
event.currentTarget is the element whose listener is running right now.
That means if you click a button inside a panel:
- the target may be the button
- the current target may be the panel if the panel's handler is running
That distinction is extremely useful in bubbling code and event delegation.
panel?.addEventListener('click', (event) => { const target = event.target as HTMLElement; const currentTarget = event.currentTarget as HTMLElement; console.log(target.className); console.log(currentTarget.className);});If you blur those two ideas together, propagation bugs become much harder to reason about.
Why Bubbling is so Useful for Event Delegation
Event delegation works because bubbling exists.
Instead of attaching a click handler to every individual child element, you can attach one handler higher up and inspect the event target.
const list = document.querySelector('.results');list?.addEventListener('click', (event) => { const target = event.target as HTMLElement; const button = target.closest<HTMLButtonElement>('.remove-button'); if (!button) { return; } console.log('Remove item', button.dataset.id);});That pattern is powerful because it keeps the number of listeners down and continues to work for matching elements added later.
It also explains why understanding bubbling properly matters. Event delegation is not a trick layered on top of the DOM. It is just a practical use of the normal event flow.
When Capturing is Actually Useful
Most front‑end code leans more heavily on bubbling, but capturing still has its place.
Capturing can be useful when:
- you want an ancestor to observe an event early
- you need a handler to run before bubbling listeners lower in the tree
- you are building infrastructure‑style behaviour such as analytics or global interaction handling
It is not something most beginners need every day, but it is helpful to know it exists so capture‑phase handlers do not feel bizarre when you eventually meet them.
stopPropagation() makes more sense once you know the phases
Developers often reach for event.stopPropagation() when event flow feels messy, but it helps to understand what it is actually stopping.
It stops the event continuing through the propagation path.
That means it can prevent ancestor listeners from running later in the journey. Used carefully, that can be useful. Used casually, it can make event behaviour much harder to understand because other code higher up silently stops seeing the interaction.
Knowing the event phases makes this decision less blunt. Instead of randomly blocking propagation, you can be more deliberate about where and why you are interrupting the path.
The Browser is Not Improvising
This is the main lesson beginners need.
When several handlers run around a single interaction, the browser is not guessing. It is following a defined path:
- down during capture
- at the target
- back up during bubble
If your code seems to behave strangely, the strange part is usually your mental model, not the browser.
Wrapping up
Event bubbling and capturing are simply two parts of the DOM event propagation model. Bubbling is the upward phase most developers notice first, whilst capturing is the downward phase that runs before the target is reached. Once you understand those phases, event order, delegation, and propagation bugs become much easier to reason about.
Key Takeaways
- DOM events move through capture, target, and bubble phases.
- Bubbling is the default phase used by most
addEventListenercalls. - Capturing runs first when you register a listener with
{ capture: true }. event.targetis where the event happened;event.currentTargetis where the current listener is attached.- Event delegation works because bubbling allows ancestor elements to observe child interactions.
Once you understand the full journey an event takes through the DOM, the behaviour stops feeling magical and starts feeling predictable.
Related Articles

Leveraging JavaScript Frameworks for Efficient Development. 
Stopping Propagation vs. Preventing Default in JavaScript. Stopping Propagation vs. Preventing Default in JavaScript

Event Delegation in JavaScript. Event Delegation in JavaScript

Solving the LeetCode 'Binary Tree Zigzag Level Order Traversal' Problem. Solving the LeetCode 'Binary Tree Zigzag Level Order Traversal' Problem

Default Parameters in JavaScript in More Depth. Default Parameters in JavaScript in More Depth

Simplify Your Layout CSS with place‑items. Simplify Your Layout CSS with
place‑items
Resolving mini‑css‑extract‑plugin Warnings in Gatsby. Resolving
mini‑css‑extract‑pluginWarnings in Gatsby
React Error Boundaries Explained. React Error Boundaries Explained

Harnessing the Power of Prototype.bind(). Harnessing the Power of
Prototype.bind()
A Brief Look at JavaScript’s Temporal Dates and Times API. A Brief Look at JavaScript's
TemporalDates and Times API
Testing Vue Components with Vue Test Utils. Testing Vue Components with Vue Test Utils
Advanced Sass: Loops. Advanced Sass: Loops