A journey of events
October 17, 2025

Hi, recently I had a task at work related to the behavior when we use Escape to close an opening file preview. We have a drawer (from a UI library), and from within the drawer we can open a custom file preview. When we press Escape, we expect to close the file preview, but instead the drawer closes while the file preview remains on the screen. You can visualize it as:
---- document
|
|---- drawer
|
|---- file preview
Interesting challenge!
Our Hotfix
In the implemented file preview, I added an event listener to the file preview component:
After checking the implementation, I found that it never reached the handler of the file preview—the console showed nothing. My senior suggested trying to add true at the end of the addEventListener:
Now it works! Let's explore why ...
The Event Journey
As we can understand, the Drawer has its own escape event handler, so this is about handler priority. When there are 2 event handlers for the same event, which one will be handled first? This comes down to the concepts of event capturing and event bubbling.
Let's consider this case: we have an outer component with its handler for a click event, and an inner component with another handler for a click event:
---- outer component
|
|---- inner component
When we click the inner component, what will be the sequence, or the event order?
- If the triggers the outer component first, then the inner, we have event capturing
- If the triggers the inner component first, then the outer, we have event bubbling
In the past, different browsers supported different concepts, but nowadays, they support both.
Event Capturing
With event capturing, the event triggers from the outside to the target of the action. In our case: from the outermost component—if any (e.g., document/window)—then from outer to inner
EVENT CAPTURING
||
---- document ||
| ||
|---- outer component ||
| ||
|---- inner component \/
Event Bubbling
With event bubbling, the event triggers from the target of the action to the outside. In our case: from inner to outer, then to the outermost component—if any (e.g., document/window)
---- document /\
| ||
|---- outer component ||
| ||
|---- inner component ||
||
EVENT BUBBLING
Hybrid Model
As I mentioned, nowadays browsers support both mechanisms. First it captures down to the target, then once it reaches the target, it bubbles up again.
HYBRID MODEL
|| /\
---- document || ||
| || ||
|---- outer component || ||
| || ||
|---- inner component \/ ||
How can we control which mechanism we're using? The browser provides this control via the third parameter as a boolean inside addEventListener. According to the documentation:
A boolean value indicating whether events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. Events that are bubbling upward through the tree will not trigger a listener designated to use capture.
Tldr, this parameter determines whether you're using capturing or not:
- addEventListener(type, listener,
true): this applies event capturing - addEventListener(type, listener): this applies event bubbling (
falseis the default)
With My Case:
Cool, enough theory, let's see how it works in practice. At the beginning, I applied:
There are 2 key things to focus on here:
- I added the event handler to document, the outermost component
- I passed nothing to the third argument, which defaults to false, thus applying event bubbling
Let's visualize this with a diagram:
---- document <-- my preview handler is here /\
| ||
|---- drawer <-- triggered here and stop, no bubble up ||
| ||
|---- file preview ||
||
EVENT BUBBLING
At the drawer level, they stop the event from bubbling up by using event.stopPropagation();. This stops the event from propagating to the next step, whether in the capturing or bubbling phase. So in my case, if I want to close only the file preview by using Escape while keeping the drawer open, this is the solution:
Let's see how this works:
EVENT CAPTURING
||
---- document <-- my preview handler is here (1) ||
| ||
|---- drawer <-- can not be triggered (2) ||
| ||
|---- file preview <-- will be closed (3) \/
Step by step:
(1): We attached the event handler for closing the file preview here, and we're using capture mode, so this will be triggered first. We then stop the event from propagating further down by using stopPropagation
(2): This will not be triggered, since we stopped the event propagation at step (1)
(3): The file preview is the innermost component, but since we attached the handler at the document level, only the file preview will be closed.
Nice!
Conclusion
This was an interesting investigation to follow through. To be honest, I should have learned about this sooner, but better late than never. If you're having the same issue, I understand your frustration, and I hope this explanation helps you. Happy coding!