Skip to content

RESOURCES / BLOG

What is Event Bubbling in JavaScript?

You click a button inside a card inside a grid, and your console suddenly prints logs from multiple listeners. If that sounds familiar, you have run into JavaScript’s event propagation model.

Hi all,

I keep seeing multiple click handlers fire when I click a nested element in my app. I’ve heard this is related to “bubbling,” but I’m not fully sure what that means.

What is event bubbling in JavaScript? How is it different from capturing, when should I use event delegation, and how do I stop propagation cleanly without breaking other listeners?

Event bubbling (sometimes called “event propagation”) is how the browser dispatches events through the DOM. It happens in three phases: capturing, target, and bubbling. Understanding these phases makes UI code predictable and helps you structure efficient listeners. Let’s break it down:

  • Capturing phase. The event travels from window down to the target’s ancestors.
  • Target phase. The event runs listeners on the exact target node.
  • Bubbling phase. The event travels back up from the target to window, triggering listeners on ancestors.
<div id="outer">
  <div id="inner">
    <button id="btn">Click me</button>
  </div>
</div>

<script>
const log = msg => console.log(msg);

// Capturing on outer
outer.addEventListener('click', () => log('outer capture'), { capture: true });

// Target and bubble listeners
outer.addEventListener('click', () => log('outer bubble'));
inner.addEventListener('click', () => log('inner bubble'));
btn.addEventListener('click', () => log('button target'));

// Clicking the button logs:
// "outer capture" -> "button target" -> "inner bubble" -> "outer bubble"
</script>Code language: HTML, XML (xml)
  • event.stopPropagation() stops the event from moving to other nodes.
  • event.stopImmediatePropagation() also stops other listeners on the same node from running.
  • event.preventDefault() stops default browser behavior but does not stop propagation.
btn.addEventListener('click', e => {
  e.preventDefault();    // stop default, e.g., link navigation or form submit
  e.stopPropagation();   // stop bubbling further up
  // handle the button click here
});Code language: PHP (php)

With delegation, you attach one listener to a parent container and handle events for child elements by checking the event target. This is great for dynamic lists or grids where items are added or removed.

const grid = document.querySelector('.grid');

grid.addEventListener('click', e => {
  const cardBtn = e.target.closest('[data-card-action]');
  if (!cardBtn || !grid.contains(cardBtn)) return; // click outside
  // Handle action for the clicked card
});Code language: JavaScript (javascript)

Benefits of delegation:

  • Fewer listeners attached to the DOM.
  • New elements work automatically without rebinding.
  • Cleaner teardown and fewer leaks.
  • Use { capture: true } if you must intercept early, but prefer bubbling for most app logic.
  • Prefer stopPropagation sparingly. Overusing it can make behavior hard to reason about.
  • Use { passive: true } for scroll or touch listeners to keep the main thread responsive.
  • In Shadow DOM contexts, use event.composedPath() to see the true dispatch path.
  • Use AbortController for cleanup: element.addEventListener('click', handler, { signal: controller.signal }).

Imagine a gallery that lazily loads images and opens a lightbox on click. You can serve responsive images and progressive enhancements, then rely on delegation for clicks. For responsive markup and best practices, see the HTML image tag guide.

<div class="grid">
  <img class="thumb"
      data-full="https://res.cloudinary.com/demo/image/upload/w_1200,c_fill,q_auto,f_auto/sample.jpg"
      src="https://res.cloudinary.com/demo/image/upload/w_300,c_fill,q_auto,f_auto/sample.jpg"
      alt="Sample">
  <!-- repeat for more thumbs, items can be added dynamically -->
</div>

<div id="lightbox" hidden>
  <img id="lightbox-img" alt="">
</div>

<script>
const grid = document.querySelector('.grid');
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');

grid.addEventListener('click', e => {
  const img = e.target.closest('.thumb');
  if (!img || !grid.contains(img)) return;

  // Swap to a larger Cloudinary transformation for the modal
  lightboxImg.src = img.dataset.full;
  lightbox.hidden = false;
});

// Optional: close on backdrop click using delegation too
lightbox.addEventListener('click', e => {
  if (e.target === lightbox) lightbox.hidden = true;
});
</script>Code language: JavaScript (javascript)

This approach scales well because you attach one click listener to the grid, not to each thumbnail. Cloudinary’s URL-based transformations let you request appropriate sizes on demand while delegation keeps your event code simple and fast.

  • Event bubbling is the phase where events travel from the target up through ancestors.
  • Use capturing only when you need to intercept early; stick to bubbling for most logic.
  • Delegate events on a common ancestor to reduce listeners and support dynamic content.
  • Use preventDefault for default behavior and stopPropagation sparingly for control.
  • For media-heavy UIs, combine delegation with optimized image delivery and responsive markup.

Want to optimize, transform, and deliver images and video while keeping your frontend code simple? Sign up for Cloudinary free and start building faster, media-rich experiences today.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free