DEV Community

Achla
Achla

Posted on

🫧 Event Bubbling & Delegation in JavaScript

"Why is my button click triggering a parent div click too? πŸ˜΅β€πŸ’«"

If you've ever asked this, welcome to the world of event bubbling.

Let me walk you through this like we’re sitting down, pair programming β€” and I’m explaining how things flow in the browser DOM with events.


🌊 What is Event Propagation?

When a browser handles events, it does so in three phases:

  • Capturing phase: event travels from root to target (also called trickling)
  • Target phase: event hits the actual target element
  • Bubbling phase: event travels back up to root

Think of event propagation like a rock dropped in water:

  • Capturing: The rock sinks (top β†’ target).
  • Bubbling: Bubbles rise back up (target β†’ top).

πŸƒ Scene 1: Just a Click

πŸ“¦ Example HTML

<div class="grand-parent">
  <div class="parent">
    <div class="child">Click Me</div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Let's Attach Listeners

const grandParent = document.querySelector('.grand-parent');
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');

grandParent.addEventListener('click', () => {
  console.log("Grandparent clicked");
});

parent.addEventListener('click', () => {
  console.log("Parent clicked");
});

child.addEventListener('click', () => {
  console.log("Child clicked");
});

Enter fullscreen mode Exit fullscreen mode

Now if you click the button, here’s what logs:

Child clicked
Parent clicked
Grandparent clicked
Enter fullscreen mode Exit fullscreen mode

Why?

πŸ‘‰ This is event bubbling.
When an event occurs on an element, it bubbles up the DOM tree β€” triggering listeners on parent elements too.

The event fires on child, then bubbles up to parent and grand-parent.


πŸ”„ Capturing Phase? Use { capture: true }

// Only grandParent and parent have { capture: true }  

grandParent.addEventListener('click', () => console.log("Captured Grandparent"), true);   // or { capture: true }

grandParent.addEventListener('click', () => console.log("Bubbled Grandparent"));  

parent.addEventListener('click', () => console.log("Captured Parent"), true);  

parent.addEventListener('click', () => console.log("Bubbled Parent")); 

child.addEventListener('click', () => console.log("Bubbled Child"));

Enter fullscreen mode Exit fullscreen mode

πŸ“‹ Now Output becomes:

Captured Grandparent  
Captured Parent  
Bubbled Child  
Bubbled Parent  
Bubbled Grandparent  

Enter fullscreen mode Exit fullscreen mode

This is the capturing phase, where events are caught while coming down the DOM tree.


🌊 What Exactly is Event Bubbling?

Imagine you're blowing bubbles underwater.
The event happens at the deepest element (button), and it bubbles up to the top (parent elements).

That’s how the browser handles events by default β€” in the bubbling phase.


πŸ›‘ Can I Stop the Bubble?

Absolutely. You can stop events from bubbling or capturing using:

event.stopPropagation();
Enter fullscreen mode Exit fullscreen mode

Or even more strictly:

event.stopPropagation();
Enter fullscreen mode Exit fullscreen mode

πŸ” Difference?

Method Prevents Parent Listeners? Prevent other listeners on same element
stopPropagation() βœ… Yes ❌ No
stopImmediatePropagation() βœ… Yes βœ… Yes

🎯 event.target vs event.currentTarget vs this

Term Meaning
event.target The actual element clicked
event.currentTarget The element with the listener
this Same as currentTarget (in regular functions)
child.addEventListener('click', function (e) {
  console.log('Target:', e.target);        // what was clicked
  console.log('CurrentTarget:', e.currentTarget); // where listener is attached
  console.log('This:', this);              // same as currentTarget
});
Enter fullscreen mode Exit fullscreen mode

πŸš€ Enter Event Delegation (Big Brain Move)

I had 100 buttons to add click listeners to.
I was doing this:

document.querySelectorAll('.box').forEach((box) => {
  box.addEventListener('click', () => {
    console.log('Box clicked');
  });
});
Enter fullscreen mode Exit fullscreen mode

Worked fine. But slow 🐌
Instead, I did:

document.querySelector('.container').addEventListener('click', (e) => {
  if (e.target.classList.contains('box')) {
    console.log('Clicked:', e.target.textContent);
  }
});
Enter fullscreen mode Exit fullscreen mode

Now the parent handles it all, thanks to event bubbling!


πŸ’‘ Real-Life Analogy

You’re in a restaurant.
Instead of each table ordering from the chef directly (many listeners),
they tell the waiter (one listener) who passes it along.

Event delegation is that waiter.

Without delegation:
πŸ§‘πŸ³ Chef (browser) runs around taking 100 individual orders (listeners).
With delegation:
πŸ§‘πŸ’Ό Waiter (parent) takes all orders in one trip.


βœ… Why Use Event Delegation?

Pros Cons
βœ… Less memory usage ❌ Can catch unintended clicks
βœ… Dynamically added elements work ❌ Harder to debug sometimes
βœ… Better for long lists or dynamic UIs ❌ Needs conditional checks

🧩 Real Use Case

Say you're building a TODO app where new tasks are added dynamically. Using delegation:

document.querySelector("#todoList").addEventListener("click", (e) => {
  if (e.target.classList.contains("delete-btn")) {
    e.target.closest("li").remove();
    console.log("πŸ—‘οΈ Task deleted");
  }
});
Enter fullscreen mode Exit fullscreen mode

Works even for tasks added after page load! πŸ”₯


❗ When Not to Use Delegation

There are cases where event delegation isn’t ideal:

  1. Isolated Components
    Think of popups, modals, or small widgets. Each has a limited scope β€” no need for a parent to listen.

  2. High-Frequency Events
    Events like mousemove, scroll, or input can fire rapidly. Delegating these may create a performance bottleneck.

  3. Security or Specificity Needs
    If each element needs to behave very differently or securely, it's better to bind individually.

  4. Form Elements with Blur/Focus
    These events don’t bubble, so delegation won't catch them!


<div id="parent">
  <button>Click <span>me</span></button>
</div>
Enter fullscreen mode Exit fullscreen mode
parent.addEventListener('click', (e) => {
  if (e.target.tagName === 'BUTTON') { 
    console.log("Button clicked!"); 
  }
});
Enter fullscreen mode Exit fullscreen mode

❌ Fails if user clicks the inside the button!
βœ… Fix: Use e.target.closest('button') instead.


πŸ“š Quick Summary

Concept What to Remember Example/Tip
Bubbling Events rise up (default). child β†’ parent β†’ grandparent
Capturing Events trickle down ({ capture: true }). grandparent β†’ parent β†’ child
event.target Deepest clicked element. Button inside a div? target = button.
Delegation Parent handles dynamic children. parent.addEventListener(..., if (e.target.matches('.child')))
stopPropagation() Stops parent listeners. e.stopPropagation() in child skips parent.

For deeper dives: MDN Event Propagation Guide

Let the events bubbleβ€”but you control the flow like a pro. πŸ§ πŸ’‘


Would love to hear how you use event delegation in your projects!

Top comments (0)