Events are everywhere in web programming — input change, mouse move, button click, and page scroll are all forms of events. These are the actions that get generated by the system so that you can respond to them however you like by registering event listeners.
This results in an interactive experience for the user. Understanding how the event model works in modern web browsers can help you build robust UI interactions. Get it wrong, and you have bugs crawling around.
My aim through this article is to elaborate some basics around the event propagation mechanism in the W3C event model. This model is implemented by all modern browsers.
Let's get started ⏰.
Event propagation
Imagine If we have two HTML elements, element1 and element2, where element2 is the child of element1 as shown in the figure below:
And we add click handlers to both of them like this:
element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));
What do you think will be the output when you click element2? 🤔
The answer is element2 is clicked
, followed by element1 is clicked
. This phenomenon is known as Event bubbling, and it's a core part of the W3C event model.
In event bubbling the innermost target element handles the event first, and then it bubbles up in the DOM tree looking for other ancestor elements with registered event handlers.
💡 In event bubbling the innermost target element handles the event first and then it bubbles up in the DOM tree
Now, the interesting bit is that event flow is not uni-directional, as you might have assumed. The event flow mechanism in the W3C event model is Bi-directional. Surprise Surprise! 😯.
We mostly have been dealing with event bubbling when working with frameworks like React and never think much of another phase which is Event Capturing.
💡 Event bubbling is just one side of the coin; Event capturing is the other.
In the event capturing phase, the event is first captured until it reaches the target element (event.target
). And you, as a web developer, can register your event handler in this phase by setting true
as the third argument inside the addEventListener
method.
// With addEventListener() method, you can specify the event phase by using `useCapture` parameter.
addEventListener(event, handler, useCapture);
By default, it's false indicating that we are registering this event in the bubbling phase.
Let's modify our example above to understand this better.
// Setting "true" as the last argument to `addEventListener` will register the event handler in the capturing phase.
element1.addEventListener('click', () => console.log('element1 is clicked'), true);
// Whereas, omitting or setting "false" would register the event handler in the bubbing phase.
element2.addEventListener('click', () => console.log('element2 is clicked')));
We have added true
for useCapture
parameter indicating that we are registering our event handler for element1 in the capturing phase. For element2, omitting or passing false
will register the event handler in the bubbling phase.
Now, if you click element2, you will see element1 is clicked
is printed first followed by element2 is clicked
. This is the capturing phase in action.
💡 In the event capturing phase, the event is first captured until it reaches the target element
Here's the diagram to help you visualise this easily:
The event flow sequence is:
- The "click" event starts in capturing phase. It looks if any ancestor element of element2 has
onClick
event handler for the capturing phase. -
The event finds element1, and invokes the handler, printing out
element1 is clicked
. - The event flows down to the target element itself(element2) looking for any other elements on its way. But no more event handlers for the capturing phase are found.
- Upon reaching element2, the bubbling phase starts and executes the event handler registered on element2, printing
element2 is clicked
. - The event travels upward again looking for any ancestor of the target element(element2) which has an event handler for the bubbling phase. This is not the case, so nothing happens.
So, the key point to remember here is that the whole event flow is the combination of the event capturing phase followed by the event bubbling phase. And as an author of the event handler, you can specify which phase you are registering your event handler in. 🧐
With this new knowledge in our bag, it's time to look back to our first example and try to analyse why the output was in reverse order. Here's the first example again so that you're not creating a scroll
event 😛
element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));
Omitting the useCapture
value registered the event handlers in the bubbling phase for both the elements. When you clicked element2, the event flow sequence was like:
- The "click" event starts in capturing phase. It looks if any ancestor element of element2 has
onClick
event handler for capturing phase and doesn't find any. - The event travels down to the target element itself(element2). Upon reaching element2, the bubbling phase starts and executes the event handler registered on element2, printing
element2 is clicked
. - The event travels upwards again looking for any ancestor of the target element(element2) which has an event handler for the bubbling phase.
- This event finds one on element1. The handler is executed and
element1 is clicked
is printed out.
Another interesting thing you can do is logging out the eventPhase property of the event. This helps you visualise which phase of the event is currently being evaluated.
element1.addEventListener("click", (event) =>
console.log("element1 is clicked", { eventPhase: event.eventPhase })
);
Here's the codepen demo if you like to play with it. Or you can paste the code snippet below in your browser and see it yourself.
const element1 = document.createElement("div");
const element2 = document.createElement("div");
// element1: Registering event handler for the capturing phase
element1.addEventListener(
"click",
() => console.log("element1 is clicked"),
true
);
// element2: Registering event handler for the bubbling phase
element2.addEventListener("click", () => console.log("element2 is clicked"));
element1.appendChild(element2);
// clicking the element2
element2.click();
Stopping the event propagation
If you wish to prevent further propagation of current event in any phase, you could invoke stopPropagation method available on the Event
object.
So, it means invoking the event.stopPropagation()
inside the element1 event handler (in capturing phase), would stop the propagation. And if even if you click element2 now, it won't invoke its handler.
The following example demonstrates that:
// Preventing the propagation of the current event inside the handler
element1.addEventListener(
"click",
(event) => {
event.stopPropagation();
console.log("element1 is clicked");
},
true
);
// The event handler for the element2 will not be invoked.
element2.addEventListener('click', () => console.log('element2 is clicked'));
Note that event.stopPropagation
stops the propagation only. It does not, however, prevent any default behaviour from occurring. For example, clicking on links are still processed. To stop those behaviours, you can use event.preventDefault()
method.
Finally, here's another cool JSbin demo if you like to play along and see how can you stop the event propagation via event.stopPropagation
.
I hope this article was helpful and has given you some insights. Thanks for reading 😍
Useful resources:
- Introduction to "DOM Events" - (whatwg specs)
- Introduction to events - (Mozilla documentation)
- Event phases and stop propagation demo - (JSbin demo)
Top comments (15)
TIL about
eventPhase
. It really doesn't matter how long you're in this profession, you really do learn something new every day.Totally agree on this one. 👍
Thank you for this! I had a bad interview a month ago where the question came up as to how to execute the parent node handler first and despite 20 years of experience I really could not remember. There just hasn’t been a scenario I’ve needed to do this and my past reading of
useCapture
in the docs was not coming to me. Today I got the answer and it was a wonderful reminder. Thank you!The only thing that could make this post better might be an example of how this might be useful. I can imagine some analytics cases but that’s not direct functionality.
Thanks for your feedback. I am sorry to hear about your interview.
Regarding the
capturing phase
, I haven't seen/found any practical use cases of it yet.But the
bubbling phase
is the core of event delegation.Interestingly, React uses this mechanism to attach all of your event handlers at the root DOM container (v17).
What is interesting here is that Phil failed his interview for not remembering something that IS NOT USEFUL, or maybe is in some very weird corner case...
Anyway, nice article Amandeep!
Thanks for your comment. 🙂👍
I am surprised that someone could test you based on if you know how event capturing works. These things are not what we do everyday and thus hard to come by, or you just forget. And that's why docs are there when you need a refresher.
I totally don't condone these interviews. I personally would stay away from these if I could.
Agreed. It's stupid to expect someone to know everything there is to know about web technologies. There is so much to know (and then more) that questioning someone without the ability to google something is pointless. A better test would be to look at how someone would solve a particular problem with all the tools at their disposal.
Exactly this. Testing how a candidate can memorise and regurgitate documentation is useless. Test how they solve problems and how they interact with you and your team. This is how you find good developers that your team can work with (which is just as, if not more, important as the candidates skillset).
Peculiar how React's synthetic event system ignores the handleEvent portion of the EventListener interface which allows objects to listen to events.
JS Fiddle
Seems Preact is looking into adding support for it (see also radEventListener: a Tale of Client-side Framework Performance).
Wow...really helpful👍
wow didn't have any idea about this. Thanks.
Love this article Clear, Complete and Consise.
Thank you. Glad that it was helpful 🙂
Very helpful post, thanks. Should the last comment in the last code block say element2?
Thanks for the feedback and that Typo 🙂👍. Cheers