Describe event capturing in JavaScript
The short answer
Event capturing is the first phase of the DOM event flow, where an event travels downward from the document through the ancestor chain until it reaches the target element. It's the opposite direction of bubbling. Most developers work exclusively with the bubbling phase, but capturing exists and runs first — before the event ever reaches the element that triggered it.
The three phases of event flow
When you click a button (or trigger any event), the browser doesn't just fire the event on that element. There's a full three-phase journey:
- Capturing phase — the event starts at the top (
window→document→<html>→<body>→ ...) and travels down toward the target element - Target phase — the event arrives at the element where it actually happened
- Bubbling phase — the event travels back up from the target through every ancestor, all the way to
document
The important thing to understand is that this entire round trip happens for every single event. The browser always does capture-then-bubble. The question is just which phase your handler listens to.
How to listen during the capture phase
By default, addEventListener registers handlers for the bubbling phase. To listen during capturing instead, you pass a third argument:
element.addEventListener('click', handler, true);Or the more readable options object:
element.addEventListener('click', handler, {
capture: true,
});That true or { capture: true } tells the browser: "Run this handler on the way down, not on the way up."
Seeing it in action
Let's say you have this structure:
<div id="outer">
<div id="inner">
<button id="button">Click me</button>
</div>
</div>Now attach handlers in both phases:
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const button = document.getElementById('button');
outer.addEventListener(
'click',
() => console.log('outer capture'),
true
);
inner.addEventListener(
'click',
() => console.log('inner capture'),
true
);
button.addEventListener('click', () =>
console.log('button target')
);
inner.addEventListener('click', () =>
console.log('inner bubble')
);
outer.addEventListener('click', () =>
console.log('outer bubble')
);When you click the button, the console output is:
outer capture
inner capture
button target
inner bubble
outer bubbleThe capture handlers fire first, from the outside in. Then the target's handler fires. Then the bubble handlers fire, from the inside out. This is the complete event lifecycle for every DOM event.
Why capturing is rarely used
Most event handling patterns rely on bubbling. Event delegation (attaching a single handler to a parent to handle events from its children) depends on events traveling upward. Frameworks like React handle events at the root level using bubbling. The overwhelming majority of real-world code uses the default bubbling phase.
So why does capturing exist at all?
When capturing is actually useful
There are a few patterns where intercepting events on the way down is exactly what you need:
Click-outside to close. If you want to detect clicks outside a dropdown or modal, a capture-phase listener on document lets you intercept the click before it reaches any element. You can check if the click target is outside your component and close it:
document.addEventListener(
'click',
(event) => {
if (!dropdown.contains(event.target)) {
closeDropdown();
}
},
true
);Using capture here means your handler runs before any other click handler on the page, so you get first dibs on deciding what to do.
Intercepting before the target handles it. Sometimes you want a parent to make a decision before the child gets the event. For example, a form wrapper might want to block clicks on certain buttons under specific conditions:
form.addEventListener(
'click',
(event) => {
if (isSubmitting) {
event.stopPropagation();
}
},
true
);Because this runs in the capture phase, stopPropagation() here prevents the event from ever reaching the target button or any of its handlers.
Focus management. The focus and blur events don't bubble. If you need to know when anything inside a container gains or loses focus, you can listen for focus during the capture phase on the container:
container.addEventListener(
'focus',
(event) => {
console.log(
'Something inside gained focus:',
event.target
);
},
true
);This works because capturing doesn't depend on the event bubbling — the event still travels down through ancestors during the capture phase even if it won't bubble back up.
stopPropagation in the capture phase
Here's where things get interesting. If you call event.stopPropagation() during the capture phase, the event stops traveling down. It never reaches the target, and the bubbling phase never happens at all.
outer.addEventListener(
'click',
(event) => {
event.stopPropagation();
console.log(
'Stopped during capture — nothing else fires'
);
},
true
);
button.addEventListener('click', () => {
console.log('This never runs');
});This is powerful but dangerous. Stopping propagation during capture can silently break event handlers that other parts of your code (or third-party libraries) are counting on. Use it sparingly and with clear intent.
The relationship to bubbling
Capturing and bubbling are two halves of the same event flow. You can't have one without the other — the browser always does the full round trip. The difference is just which phase your listener responds to.
If you understand bubbling well, capturing is straightforward: it's the same journey in the opposite direction, and it happens first. Together they give you complete control over when and where you intercept events in the DOM tree.
Why interviewers ask this
Capturing questions test whether you understand the full event model, not just the commonly used parts. Most developers know about bubbling, but explaining the capture phase shows deeper knowledge of how the browser processes events. It also connects to practical patterns like click-outside detection and focus management, which are common in production applications.