Event Delegation
Event delegation is a JavaScript feature that passes events from child elements to a parent element, allowing you to handle events more efficiently and work with dynamically added elements.
Understanding Event Flow
Before we dive into event delegation, we need to understand how events flow through the DOM. When you click on an element, you're actually clicking on multiple elements at once!
<html>
<body>
<div class="container">
<button>Click Me</button>
</div>
</body>
</html>
When you click the button, you're also clicking on:
- The
divcontainer (the button is inside it) - The
bodyelement (the div is inside it) - The
htmlelement (the body is inside it) - The
document(everything is inside it)
Event Flow Phases
Events flow through the DOM in three phases:
- Phase 1 - Capture: The event travels from the document down to the target element
- Phase 2 - Target: The event reaches the actual target element
- Phase 3 - Bubble: The event travels back up from the target to the document
Seeing Event Bubbling in Action
By default events trigger in the bubble phase, meaning they start at the target element and bubble up to its ancestors.
<div id="outer">
<div id="middle">
<button id="inner">Click Me</button>
</div>
</div>
const outer = document.querySelector("#outer")
const middle = document.querySelector("#middle")
const inner = document.querySelector("#inner")
outer.addEventListener("click", () => {
console.log("Outer div clicked")
})
middle.addEventListener("click", () => {
console.log("Middle div clicked")
})
inner.addEventListener("click", () => {
console.log("Button clicked")
})
// When you click the button, you'll see:
// "Button clicked"
// "Middle div clicked"
// "Outer div clicked"
The event starts at the button (target) and bubbles up through its ancestors.
Controlling Event Flow
You may not want this default bubbling behavior, so there are a few ways to control it:
Stopping Event Propagation - stopPropagation
You can stop an event from propagating to other elements by calling stopPropagation() on the event object:
<div id="outer">
<div id="middle">
<button id="inner">Click Me</button>
</div>
</div>
inner.addEventListener("click", (e) => {
console.log("Button clicked")
e.stopPropagation() // Stop the event here!
})
middle.addEventListener("click", () => {
console.log("This won't run!")
})
outer.addEventListener("click", () => {
console.log("This won't run either!")
})
// When you click the button, you'll see:
// "Button clicked"
Capture Phase Event Listeners
By default, event listeners run during the bubble phase, but you can make them run during the capture phase instead:
<div id="outer">
<button id="inner">Click Me</button>
</div>
// This runs during the CAPTURE phase (document → target)
outer.addEventListener(
"click",
() => {
console.log("Outer div clicked (capture)")
},
{ capture: true }
)
// This runs during the BUBBLE phase (target → document)
inner.addEventListener("click", () => {
console.log("Button clicked (bubble)")
})
// When clicking the button, you'll see:
// "Outer div clicked (capture)"
// "Button clicked (bubble)"
When To Use Each Phase
By default you should use the bubble phase for most event listeners, but the capture phase can be useful if you want to stop an event before it reaches the target element.
<div id="outer">
<button id="inner">Click Me</button>
</div>
outer.addEventListener(
"click",
(e) => {
e.stopPropagation() // Stop the event from reaching the target
},
{ capture: true }
)
inner.addEventListener("click", () => {
console.log("This never runs!")
})
Not All Events Can Be Delegated
Pretty much all events go through the normal bubble/capture phases, but some events don't bubble up. focus and blur are the most common events that do not bubble.