When you click a button inside a card inside a page, you really clicked all three of them. The browser reports the event to each one, from the inside out. That is bubbling, named after bubbles rising in water.
<div id="card" style="padding:14px;background:#fdf2e9">
Card
<button id="btn">Button</button>
</div>
<script>
document.querySelector("#btn").addEventListener("click", () => console.log("1. button"));
document.querySelector("#card").addEventListener("click", () => console.log("2. card"));
document.addEventListener("click", () => console.log("3. document"));
</script>
Click the button. The console shows 1. button, then 2. card, then 3. document.
The three phases
Every event goes through:
- Capturing: from
windowdown to the target. - Target: at the element itself.
- Bubbling: from the target back up to
window.
By default, listeners run in the bubbling phase. To listen while the event is going down, pass true or { capture: true }:
card.addEventListener("click", handler, { capture: true });
You will rarely need capturing. Know that it exists.
Bubbling is useful
Because events rise, a listener on a parent can handle events from all its children. That is event delegation, covered next: Event Delegation.
Controlling it
event.stopPropagation() stops the event from going further, as shown in preventDefault and stopPropagation. Prefer to leave events alone unless there is a reason.
Events that do not bubble
A few events do not bubble: focus, blur, mouseenter, mouseleave, load. For delegation, use their bubbling cousins focusin, focusout, mouseover and mouseout.
Which element handled it?
Use event.target for the innermost element that was clicked and event.currentTarget for the element whose listener is running: The Event Object.
A common problem: nested handlers
If both a card and a button inside it have click handlers, clicking the button runs both. Decide which one should run, check event.target in the outer handler, or restructure the code so the layers do not overlap.