Frontend Development › JavaScript & TypeScript
Event Bubbling and Capturing
Events travelling up (and down) the DOM tree.
Also known as: event propagation, capturing phase, stopPropagation
When an event happens on an element, it doesn’t stay there. It travels through the DOM tree in three phases:
- Capturing: from the window down to the target’s parent.
- Target: the element itself.
- Bubbling: back up from the target through its ancestors to the window.
By default, listeners run during the bubbling phase. So a click on a button also triggers click handlers on its parent, grandparent and so on.
<div id="card">
<button id="buy">Buy</button>
</div>
card.addEventListener("click", () => console.log("card"));
buy.addEventListener("click", () => console.log("button"));
// click the button -> "button" then "card"
Useful consequences
- Event delegation: attach one listener to a parent, and handle events from many children.
event.targetis the element that was actually clicked.event.currentTargetis the element the listener is attached to.
list.addEventListener("click", (e) => {
const item = e.target.closest("li");
if (item) select(item.dataset.id);
});
Controlling propagation
e.stopPropagation(); // stop it travelling further
e.preventDefault(); // cancel the default action (like following a link); doesn't stop bubbling
el.addEventListener("click", handler, { capture: true }); // listen in the capture phase
Cautions
- Don’t overuse
stopPropagation(). It breaks other code that relies on seeing the event, such as analytics and “click outside to close” menus. preventDefaultandstopPropagationare different things, and are often confused.- Not all events bubble.
focus,blur,mouseenterandloaddon’t (focusinandfocusoutdo). - Nested clickable elements (a button inside a clickable card) cause double actions. Check the target, or stop propagation deliberately.
- Frameworks wrap events, but the same ideas apply.