DEV Community

Cover image for Understanding Event Phases in the DOM
Ritam Saha
Ritam Saha

Posted on

Understanding Event Phases in the DOM

Introduction

Hey there, fellow developers!

If you've ever wondered why clicking on a nested element sometimes triggers multiple event handlers in a specific order — or why adding true as the third parameter to addEventListener() changes everything — you're in the right place.

Today, we're diving deep into event phases in the DOM. We'll explore the three phases of events, how addEventListener() controls them with its mysterious third parameter, and exactly what happens under the hood when you click an element.

Grab your favorite chai, and let's make this fun and crystal clear!

The Three Parameters of addEventListener()

The addEventListener() method accepts up to three arguments:

element.addEventListener(event, callback, options);
Enter fullscreen mode Exit fullscreen mode
  • event — The event name (e.g., "click", "mouseover")
  • callback — The function to run when the event fires
  • options — This is where the magic happens! It can be a boolean or an object that specifies characteristics about the event listener.

When you pass a boolean as the third argument:

  • falseBubbling phase (this is the default)
  • trueCapturing phase

This third parameter decides when your event handler should run during event propagation.

Event Bubbling vs Event Capturing

Imagine you click on a child element inside a parent. Does the event start at the child and go up? Or start from the top and come down?

  • Bubbling (default, false): The event starts at the target element and "bubbles up" to its ancestors (bottom → top).
  • Capturing (true): The event starts from the window and "captures down" to the target element (top → bottom).

Most developers only know bubbling because it's the default. But understanding both gives you superpower for complex UIs.

The Three Event Phases

Every DOM event goes through three distinct phases:

  1. Capturing Phase (top → bottom) — only runs if listeners were added with true
  2. Target Phase — the event has reached the actual element that was clicked
  3. Bubbling Phase (bottom → top) — default behavior

Here's a beautiful diagram that visualizes exactly how these phases flow:

Event Phases diagram

As you can see:

  • Left side shows bottom-up bubbling (default, false)
  • Right side shows top-down capturing (true)
  • The flow always starts from windowhtmlbodyparentchild

Now, let's see this in action with real code!

Live Demo: Understanding All Combinations

Here's our simple HTML structure:

<body>
  <div id="parent">
    <div id="child">Hello I'm Child</div>
  </div>
</body>
Enter fullscreen mode Exit fullscreen mode

We'll add click listeners to both parent and child using different combinations of the third parameter.

Case 1: Both Listeners in Capturing Phase (true)

const parent = document.getElementById("parent");
const child = document.getElementById("child");

parent.addEventListener("click", () => {
  console.log("Parent Clicked (Capturing)");
}, true);

child.addEventListener("click", () => {
  console.log("Child Clicked (Capturing)");
}, true);
Enter fullscreen mode Exit fullscreen mode

Output when clicking the child:

Parent Clicked (Capturing)
Child Clicked (Capturing)
Enter fullscreen mode Exit fullscreen mode

Why? Capturing always flow from top to bottom. Parent is higher in the DOM tree, so it fires first.

Case 2: Both in Bubbling Phase (false or omitted)

parent.addEventListener("click", () => {
  console.log("Parent Clicked (Bubbling)");
}, false);   // or just omit the third parameter

child.addEventListener("click", () => {
  console.log("Child Clicked (Bubbling)");
});
Enter fullscreen mode Exit fullscreen mode

Output:

Child Clicked (Bubbling)
Parent Clicked (Bubbling)
Enter fullscreen mode Exit fullscreen mode

Classic bubbling behavior — starts at the target (child) and goes up.

Case 3: Parent Capturing (true), Child Bubbling (false)

parent.addEventListener("click", () => console.log("Parent (Capturing)"), true);
child.addEventListener("click", () => console.log("Child (Bubbling)"));
Enter fullscreen mode Exit fullscreen mode

Output:

Parent (Capturing)
Child (Bubbling)
Enter fullscreen mode Exit fullscreen mode

Why?

Capturing phase runs top-down: the parent’s true listener fires first.

Then the target phase reaches the child, so the child’s listener (registered with false) executes.

Result: Parent first, then Child.

Case 4: Parent Bubbling (false), Child Capturing (true)

parent.addEventListener("click", () => console.log("Parent (Bubbling)"));
child.addEventListener("click", () => console.log("Child (Capturing)"), true);
Enter fullscreen mode Exit fullscreen mode

Output:

Child (Capturing)
Parent (Bubbling)
Enter fullscreen mode Exit fullscreen mode

Why?

Capturing phase runs top-down: the parent has no capturing listener, so it is skipped. When the phase reaches the child (the target), the child’s true listener fires.

After that, the bubbling phase begins (bottom-up), and the parent’s false listener executes.

How the Flow Actually Works (Step by Step)

Let’s break it down using the diagram:

  1. Capturing Phase begins at the window object and moves top → bottom (window → html → body → parent → child).

    • Only listeners added with true (or {capture: true}) will execute during this phase.
  2. Target Phase: Once the event reaches the actual clicked element (the target), all listeners on the target element itself run — regardless of whether they were registered with true (capture) or false (bubble).

  3. Bubbling Phase starts after the target phase and moves bottom → top (child → parent → body → html → window).

    • Only listeners added with false (default) will execute here.

Important note: Even if you add listeners in capturing mode and there are also other listeners on bubblinh mode, then the bubbling phase still happens unless you call event.stopPropagation().

Pro Tips for Real Projects

  • Use capturing (true) when you want to handle events before they reach the target (great for preventing default behaviors early).
  • Most of the time, bubbling (false) is what you want — it's simpler and sufficient for 90% of use cases.
  • You can also use the modern object syntax for more control:
element.addEventListener("click", handler, {
  capture: true,      // same as passing true
  once: true,         // auto-remove after first trigger
  passive: true       // performance optimization for scroll/touch
});
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Event propagation might seem confusing at first, but once you visualize it with the flow (top-down capturing → target → bottom-up bubbling), it becomes incredibly intuitive.

Now you have full control over when your event handlers run. Go ahead and experiment with all four combinations on your own. You'll be amazed at how much power this gives you!


Top comments (0)