If an element and its parent have an event handler for the same event, which element will fire first when triggered?
Propagating events in JavaScript using bubbling and capturing provides developers with an answer to this question. In this article, we’ll learn how event bubbling and capturing work, compare different methods for accessing event properties, and run through a few different examples and use cases.
Let’s get started!
In event capturing, which is also known as trickling, the outer event handler fires before the specific handler fires. For example, the event on the div triggers before the event on the button:
Event capturing hierarchy = document → html → body → parent → child
Capturing has a higher priority than bubbling, meaning that capturing event handlers are executed before bubbling event handlers, as shown by the phases of event propagation:
Event bubbling follows the opposite order as event capturing. An event propagates from a child HTML element, then moves up the DOM hierarchy to its parent elements:
Event bubbling hierarchy = child → parent → body → html → document
We can listen to these propagation events using the addEventListener()
method, which is appended to HTML nodes. It accepts three arguments: an event name, a callback function, and an optional capture value, which is set to false
by default:
element.addEventListener(event, handler, false)
Let’s review what will happen if a user clicks on the button when the capturing value is left blank:
element.addEventListener(event, handler)
The click event begins in the capturing phase. It searches the parent elements of the target for any event with the event handler. It will not find any event handler for the capturing phase.
Next, it trickles down to the target. Once all events for the capturing phase are executed, the event moves to its bubbling phase. It executes any event handler set on the target element. It propagates up again, searching through the target’s parent elements for any event handler for the bubbling phase. Now, the event cycle is complete.
true
Let’s consider what happens if the capturing value is set to true
:
element.addEventListener(event, handler, true)
The code snippet above will follow the same order as when the value was empty. The key difference is that the event handler will execute any event handler it finds before trickling down to the target.
There are differences in the methods used by handlers to access event object properties:
event.target
: refers to the DOM element that triggered the eventevent.eventPhase
: returns the current phase of event propagation (capturing: 1
, target: 2
, bubbling: 3
)event.currentTarget
: refers to the DOM element that handles the eventNote that if an event listener is attached to the parent, but event propagation is stopped by the child, event.currentTarget
refers to the DOM element that stopped the propagation.
Now that we understand how event bubbling and capturing work, let’s try an example! Let’s say we have the following DOM structure and the following event listener, respectively:
<button class="cta_button">Click me</button>
document .querySelector('.cta_button') .addEventListener('click', function(event) { console.info(`Click event fired on ${this.nodeName}`); });
Click event fired on BUTTON
would be logged to the console.
Let’s see what happens when the DOM structure is nested and uses the same event listener attached to the parent element:
<div class="cta_container"> <button class="cta_button">Watch me bubble</button> </div>
document .querySelector('.cta_container') .addEventListener('click', function(event) { console.info(`Click event fired on ${this.nodeName}`); });
In the code snippet above, we set a click event listener on the div
, the parent element of the button. When clicked, it logs the type of event fired and the element it is fired on.
When users click the Watch me bubble
button, the event is directed to the button. If an event handler is set for the button, the event is triggered. Otherwise, the event bubbles, or propagates, to the parent div
, and a click event is fired on the parent. If the event is not handled, the process continues to the next parent at the outer bound until it eventually reaches the document object.
Even though you clicked on the button, the information logged to the console is Click event fired on DIV
.
What happens when we also attach an event listener to the button?
<div class="cta_container"> <button class="cta_button">Watch me bubble</button> </div>
document .querySelector('.cta_container') .addEventListener('click', function(event) { console.info(Click event fired on ${this.nodeName}`); });
The output becomes Click event fired on BUTTON
and Click event fired on DIV
.
As you can see, the event bubbled to the parent. You can use the event.bubbles
property to ascertain whether an event bubbles:
document .querySelector('.cta_button') .addEventListener('click', function(event) { console.info( Click event fired on ${this.nodeName}. Does it bubble? ${event.bubbles}` ); });
An event on a DOM element propagates to all of its parent elements unless it is stopped. Although there is usually no need to prevent bubbling, it can be useful in certain cases. For example, stopping the propagation can prevent event handlers from interfering with each other.
Consider handling drag-and-drop using mousemove
and mouseup
events. Stopping the propagation may prevent browser bugs that arise as a result of users randomly moving the mouse.
Calling event.stopPropagation()
on the child element prevents it from bubbling to the parent element:
document.querySelector('.cta_button').addEventListener('click', event => { event.stopPropagation(); // ... });
Let’s stop the propagation for our example from the previous section where we clicked a button:
<div class="cta_container"> <button class="cta_button">Watch the bubble stop</button> </div>
Let’s add the event listeners:
document .querySelector('.cta_container') .addEventListener('click', function(event) { console.info(`Click event fired on ${this.nodeName}`); }); document .querySelector('.cta_button') .addEventListener('click', function(event) { event.stopPropagation(); console.info(`Click event fired on ${this.nodeName}`); });
The event was prevented from bubbling using event.stopPropagation()
. The output becomes Click event fired on BUTTON
.
Let’s say you want to allow the propagation to continue, but you want to prevent the browser from performing its default action if there is no listener handling the event. You can use event.preventDefault()
:
document.querySelector('.cta_button') .addEventListener('click', event => { event.preventDefault(); // ... });
Let’s apply what we’ve covered and create a shopping list app that strikes an item once you purchase it. In this scenario, adding individual event listeners won’t be feasible because you may decide to add a new item in the future.
Instead, you need to attach the event listener to the parent element of the list. The event listener will handle the click event from the children through event bubbling.
In the code snippet below, we list our shopping items:
<ul class="list"> <li class="item">MacBook Pro</li> <li class="item">Sony a6400 camera</li> <li class="item">Boya universal cardioid microphone</li> <li class="item">Light ring</li> </ul>
Add an event listener attached to <ul>
, as seen below:
document.querySelector('.list').addEventListener('click', event => { event.target.classList.toggle('purchased'); });
You can view this code in the CodePen below:
See the Pen
Event Bubbling by Chiamaka Ikeanyi (@chiamakaikeanyi)
on CodePen.
During event delegation, where event bubbling is not supported, event capturing becomes especially beneficial for attaching events to dynamic content. For example, you may need to handle events like focus
and blur
for which bubbling is not supported.
To catch an event on the capturing phase, you need to set the useCapture
option to true
. Remember, by default, it is set to false
:
element.addEventListener(event, handler, true)
Let’s consider a nested DOM structure:
<div class="cta_container"> <button class="cta_button">Watch me capture</button> </div>
We’ll set the useCapture
option of the parent element to true
:
document.querySelector('.cta_container').addEventListener( 'click', function(event) { console.info(Click event fired on ${this.nodeName}`); }, true ); document .querySelector('.cta_button') .addEventListener('click', function(event) { console.info(`Click event fired on ${this.nodeName}`); });
Contrary to what we get using bubbling, the output is Click event fired on DIV
and Click event fired on BUTTON
.
Let’s continue our example from earlier when we built a shopping list. If you wanted to add an input field to the shopping list that enables you to set a budget for each item, the event listener attached to the parent would not apply to those input fields.
Let’s take the code for our shopping list and an event listener:
<h1 class="title">Shopping List</h1> <ul class="list"> <li class="item"> MacBook Pro <input class="budget" type="number" min=1> </li> <li class="item"> Logitech MX Keys <input class="budget" type="number" min=1> </li> <li class="item"> Sony a6400 camera <input class="budget" type="number" min=1> </li> <li class="item"> Boya universal cardioid microphone <input class="budget" type="number" min=1> </li> <li class="item"> Light ring <input class="budget" type="number" min=1> </li> </ul>
document.querySelector(".list").addEventListener("focus", function (event) { console.info(${event.type} event fired on ${this.nodeName}`); event.target.style.background = "#eee"; console.log("target:", event.target); console.log("currentTarget:", event.currentTarget); console.log("eventPhase:", event.eventPhase); });
When you focus the cursor on any input field, nothing happens. However, when you set the useCapture
option to true
, you’ll achieve the desired result:
document.querySelector(".list").addEventListener("focus", function (event) { console.info(`${event.type} event fired on ${this.nodeName}`); event.target.style.background = "#eee"; console.log("target:", event.target); console.log("currentTarget:", event.currentTarget); console.log("eventPhase:", event.eventPhase); }, true);
See the Pen
Event Capturing by Chiamaka Ikeanyi (@chiamakaikeanyi)
on CodePen.
You can view this list in the CodePen above.
A strong understanding of event bubbling and capturing is essential for handling user events in JavaScript. In this tutorial, we learned how event propagation works in JavaScript, following the sequence of capturing, target phase, and bubbling.
Note that bubbling always propagates from a child element to the parent, while capturing propagates from the parent element to the child. To remember the propagation order, you can think of “bubble up and trickle down.”
I hope you enjoyed this tutorial!
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
2 Replies to "Deep dive into JavaScript event bubbling and capturing"
Thank you for this blog! Very thorough and to the point. Other blogs I read were full of verbose.
Nice article. Well written and structured content