Skip to content

Event Bubbling and Capturing — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Event Bubbling and Capturing. We cover key concepts, practical examples, and best practices to help you master this topic.

Event propagation has three phases — capture (root to target), target, and bubble (target to root) — controlling how events travel through the DOM tree and trigger listeners on ancestor elements.

What You'll Learn

  • The three phases of event propagation: capture, target, bubble
  • How to use stopPropagation and stopImmediatePropagation
  • When bubbling is useful and when it causes problems
  • How to determine which phase a listener fires in

Why It Matters

Without understanding propagation, you will encounter mysterious bugs: clicks on a child element triggering parent handlers, form submit handlers firing twice, or event delegation not working as expected.

Real-World Use

  • A modal overlay closes when clicking outside the modal content (bubbling to the overlay)
  • A table uses event delegation on the tbody to handle clicks on any row
  • A nested menu structure uses stopPropagation to prevent parent menus from closing
flowchart TD
  subgraph Capture Phase
    A1[document] --> A2[html]
    A2 --> A3[body]
    A3 --> A4[parent]
    A4 --> A5[target]
  end
  subgraph Bubble Phase
    A5 --> B4[parent]
    B4 --> B3[body]
    B3 --> B2[html]
    B2 --> B1[document]
  end

Understanding Event Phases

Every DOM event follows a three-phase journey. The event object's eventPhase property indicates the current phase.

const parent = document.querySelector('.parent');
const child = document.querySelector('.child');

// Capture phase listener (third argument true)
document.body.addEventListener('click', function(event) {
    console.log('BODY capture phase:', event.eventPhase);
    console.log('Phase value:', event.eventPhase === 1 ? 'CAPTURE' : '');
}, true);

// Target phase (fires on the clicked element)
child.addEventListener('click', function(event) {
    console.log('CHILD target phase:', event.eventPhase);
    console.log('Phase value:', event.eventPhase === 2 ? 'TARGET' : '');
});

// Bubble phase listener (default)
parent.addEventListener('click', function(event) {
    console.log('PARENT bubble phase:', event.eventPhase);
    console.log('Phase value:', event.eventPhase === 3 ? 'BUBBLE' : '');
});

Expected output: Clicking the child logs: body capture (phase 1), child target (phase 2), parent bubble (phase 3). The event travels document -> body -> child -> parent -> document.

Using stopPropagation

The stopPropagation method prevents the event from continuing to the next phase or to sibling listeners in the same phase.

const modal = document.querySelector('.modal');
const modalContent = document.querySelector('.modal-content');
const closeBtn = document.querySelector('.modal-close');

// Clicking the overlay closes the modal
modal.addEventListener('click', function() {
    console.log('Overlay clicked — closing');
    this.style.display = 'none';
});

// But clicking inside the modal content should NOT close it
modalContent.addEventListener('click', function(event) {
    event.stopPropagation();
    console.log('Content clicked — propagation stopped');
});

// Clicking the close button also stops propagation
closeBtn.addEventListener('click', function(event) {
    event.stopPropagation();
    console.log('Close button clicked');
    modal.style.display = 'none';
});

Expected output: Clicking the overlay closes the modal. Clicking the modal content does nothing to the overlay because propagation is stopped at the content level. The close button also stops propagation.

stopImmediatePropagation

Unlike stopPropagation, stopImmediatePropagation also prevents other listeners on the same element from firing.

const button = document.querySelector('.emergency-btn');

// First handler
button.addEventListener('click', function(event) {
    console.log('Handler 1');
});

// Second handler stops immediate propagation
button.addEventListener('click', function(event) {
    console.log('Handler 2');
    event.stopImmediatePropagation();
    console.log('Stopped — subsequent handlers will not fire');
});

// Third handler (never runs)
button.addEventListener('click', function(event) {
    console.log('Handler 3 — will NOT execute');
});

// With stopPropagation, Handler 3 would still run
// With stopImmediatePropagation, Handler 3 is skipped

Expected output: Clicking the button shows "Handler 1", "Handler 2", and the stop message. Handler 3 never executes because stopImmediatePropagation prevents it.

Event Delegation

Event delegation leverages bubbling to handle events on many elements with a single listener on a common ancestor.

// Without delegation: one listener per item
const listItems = document.querySelectorAll('.list-item');
listItems.forEach(item => {
    item.addEventListener('click', function() {
        console.log('Clicked:', this.textContent);
    });
});

// With delegation: single listener on parent
const list = document.querySelector('.item-list');
list.addEventListener('click', function(event) {
    // Find the closest list item (in case click was on a child element)
    const item = event.target.closest('.list-item');
    if (item) {
        console.log('Delegated click:', item.textContent);

        // Highlight the clicked item
        this.querySelectorAll('.list-item').forEach(i => {
            i.classList.remove('active');
        });
        item.classList.add('active');
    }
});

// Delegation handles items added dynamically after listener attachment
const newItem = document.createElement('li');
newItem.className = 'list-item';
newItem.textContent = 'Dynamically added';
list.appendChild(newItem);
// Clicking this new item works without attaching a new listener!

Expected output: Clicking any list item highlights it and logs its text. Dynamically added items also work because the delegate listener is on the parent, not the individual items.

Bubbling Events vs Non-Bubbling Events

Not all events bubble. Knowing which ones bubble helps you choose the right delegation Strategy.

// Bubbling events (can be delegated):
// click, mousedown, mouseup, keydown, keyup, submit, focusin, blur
// touchstart, touchend

// Non-bubbling events (cannot be delegated directly):
// focus, blur, load, unload, scroll, mouseenter, mouseleave

// Focus delegation workaround: use focusin/focusout which bubble
const form = document.querySelector('form');

// This does NOT work for dynamically added inputs:
// form.addEventListener('focus', handler); // focus does not bubble

// This works:
form.addEventListener('focusin', function(event) {
    const input = event.target;
    console.log('Input focused:', input.name);
    input.classList.add('focused');
});

form.addEventListener('focusout', function(event) {
    const input = event.target;
    input.classList.remove('focused');
});

// Mouseenter delegation workaround:
// Use mouseover and check event.target

Expected output: Focusing any input (including dynamically added ones) logs its name and adds the focused class. Blurring removes it.

Common Mistakes

  1. Calling stopPropagation unnecessarily — Stopping propagation can break other features that rely on bubbling (analytics tracking, tooltip dismissals, global click handlers).
  2. Forgetting that not all events bubble — Trying to delegate focus, blur, scroll, or load events without checking if they bubble causes silent failures.
  3. Using event.target instead of event.currentTarget inside a delegate — event.target is the element that triggered the event. event.currentTarget is the element the listener is attached to (the delegate parent).
  4. Accidentally stopping propagation in capture phase — Stopping propagation in the capture phase prevents the event from reaching the target at all.
  5. Assuming stopPropagation prevents default — stopPropagation only stops event travel. Use preventDefault to prevent the browser's default action (like following a link).

Practice Questions

  1. What are the three phases of event propagation? Capture (root to target), Target (at the element), Bubble (target back to root).
  2. What is the difference between stopPropagation and stopImmediatePropagation? stopPropagation prevents the event from moving to other elements. stopImmediatePropagation also prevents other listeners on the same element from firing.
  3. Why is event delegation more efficient than individual listeners? It uses a single listener instead of many, saving memory. It also handles dynamically added elements without reattaching listeners.
  4. Challenge: Implement a tree view component where clicking a node expands/collapses its children, clicking a child selects it, and clicking the root does nothing. Use a single delegated click listener on the tree container.

FAQ

What is the default phase for addEventListener?

The bubbling phase. The third argument defaults to false, meaning the listener fires during bubbling.

Can I listen to events in both capture and bubble phases?

Yes. Add two listeners on the same element — one with capture:true and one without. They fire at different times.

Does stopPropagation prevent other listeners on the same element?

No. It only prevents the event from moving to other elements. Use stopImmediatePropagation to stop same-element listeners.

Why does focus not bubble but focusin does?

This is a historical design decision. focusin and focusout were introduced by Microsoft and later standardized because they bubble, making delegation possible.

Can I use event delegation on the document level?

Yes. Many libraries attach a single document listener and use event.target to determine which handler to call. This is the foundation of jQuery's event system.

Mini Project

Build a todo list app using event delegation. A single ul container has one click listener. Clicking a todo item toggles its completed class. Each item has a delete button (a span with class "delete") that removes the item. An input field and add button create new todos. All interaction is handled through the single delegated listener on the ul.

What's Next

Continue with Lesson 14: Event Delegation Patterns to explore advanced delegation patterns for complex UI components.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro