cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Bubbling & capturing

Learn how browser events travel through nested elements, how target differs from currentTarget, and when stopPropagation helps or hurts.

By the end you can
  • 01
    Trace the three phasesPredict capture, target, and bubble order for nested elements.
  • 02
    Read the event objectUse target, currentTarget, eventPhase, and composedPath() accurately.
  • 03
    Stop carefullyChoose between stopPropagation, stopImmediatePropagation, and preventDefault.

The trip an event takes

When you click a button, the browser does not only tell the button. It sends one event object on a round trip through the page tree. The trip has three parts: capture on the way in, target on the element that was actually hit, and bubble on the way back out.

That sounds fancy, but it explains a lot of everyday behavior: why a card can hear a click from a button inside it, why a menu can close when the document hears an outside click, and why a careless stopPropagation() can break code far away from the line that called it.

Real-life analogyA package delivered in an apartment building

Imagine a courier carrying a package to an apartment. They enter the building, pass the lobby, go to the floor, and reach the apartment. After delivery, everyone hears the news in reverse order: apartment, floor, lobby. A click takes the same kind of trip through nested elements.

In real life: Courier enters the lobby
In JavaScript: Capture starts at the outside: window, document, page section
In real life: Courier reaches the apartment
In JavaScript: Target phase on the clicked element
In real life: News travels back to the floor and lobby
In JavaScript: Bubble phase climbs back through ancestors
In real life: Any level can react
In JavaScript: Any ancestor with a listener can run code

Where the analogy stops: Real buildings have one lobby and one staircase. Browser event paths can include shadow DOM and iframes, and composedPath() is the reliable way to inspect the exact path.

The one-sentence definition

Event propagation is the browser sending one event object through ancestors on the way to the target, then through ancestors again on the way out.

Capture, target & bubble phases

STEP THROUGH

A listener can join either side of the trip. The third argument to addEventListener chooses capture; leaving it out listens during target/bubble:

Adding capture and bubble listenersPop out in the code editor (opens in a new tab)JavaScript
section.addEventListener("click", log, { capture: true });card.addEventListener("click", log, { capture: true });button.addEventListener("click", log, { capture: true }); button.addEventListener("click", log);card.addEventListener("click", log);section.addEventListener("click", log);

For a click on the button, capture listeners run from outer to inner. Then the target’s own listeners run. In modern browsers, capture listeners registered on the target itself run before non-capturing target listeners; older browsers historically differed, so avoid code that depends on tricky same-target ordering. Finally, events whose bubbles property is true bubble from inner to outer.

The three phases
PhaseDirectioneventPhaseListener option
Capturewindow → document → ancestors → parent1 / CAPTURING_PHASE{ capture: true } or true
Targetthe actual target element2 / AT_TARGETcapture listeners, then non-capturing listeners in modern browsers
Bubbleparent → ancestors → document → window3 / BUBBLING_PHASEdefault listener option, when event.bubbles is true

The model below is intentionally plain JavaScript data, not a DOM. It lets you replay the same order tests can verify in Node, then compare with the real browser visualizer in the next section.

Replay the propagation model
Step 0 of 11Ready
Your turn: follow the blue line

Replay the event path from the pure model. Change the stop setting, then compare with the real DOM visualizer above.

Running in
  1. script
Next: line 5
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const path = ["section", "card", "toolbar", "button", "icon"];const listeners = ["capture", "bubble"]; for (const node of path.slice(0, -1)) {} runListener("button", "target capture");runListener("button", "target bubble"); for (const node of ["toolbar", "card", "section"]) {  runListener(node, "bubble");}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Where should the model call a stop method?

The visualizer uses the browser; this replay uses a small pure model so tests can verify it without a DOM.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Experiment: watch propagation in a real DOM

INTERACTIVE

Now try the browser’s real event system. The demo renders four nested boxes with React, then an effect attaches native DOM listeners to those existing nodes and removes them during cleanup. Clicking the star inside the button shows the exact order, plus eventPhase, target, and currentTarget on every line.

Propagation visualizer: click the icon inside the button
Listeners attached by the demoPop out in the code editor (opens in a new tab)JavaScript
const section = document.querySelector("[data-box='section']");const card = document.querySelector("[data-box='card']");const toolbar = document.querySelector("[data-box='toolbar']");const button = document.querySelector("[data-box='button']"); section.addEventListener("click", log, { capture: true });section.addEventListener("click", log);button.addEventListener("click", log, { capture: true });button.addEventListener("click", log); function log(event) {  console.log(event.eventPhase, event.target, event.currentTarget);}
Nested boxes0 logs
section
card
toolbar
    Try it yourself

    Click the star span inside the button. Capture listeners run top-down, then the target, then bubble listeners bottom-up.

    Real DOM listeners are attached in an effect and removed during cleanup. The second button listener proves what stopImmediatePropagation skips.

    Toggle capture off and the first half disappears. Toggle bubble off and only capture plus target listeners remain. Choose a stop point and watch later nodes stop hearing the event.

    What the visualizer attachesPop out in the code editor (opens in a new tab)JavaScript
    const section = document.querySelector("[data-box='section']");const card = document.querySelector("[data-box='card']");const toolbar = document.querySelector("[data-box='toolbar']");const button = document.querySelector("[data-box='button']"); section.addEventListener("click", log, { capture: true });section.addEventListener("click", log);button.addEventListener("click", log, { capture: true });button.addEventListener("click", log); function log(event) {  console.log(event.eventPhase, event.target, event.currentTarget);}

    target vs currentTarget

    event.target is where the event started: the deepest node the browser considers clicked. event.currentTarget is the element whose listener is running right now. In the visualizer, click the star span inside the button: logs from the section still have target as the span, but currentTarget as the section.

    Real-life analogyNesting dolls feel the poke

    Nesting dolls make bubbling easy to picture. Poke the tiny doll in the center and every shell around it can feel the poke. The original poke point stays the same; the doll currently reacting changes.

    In real life: You poke the innermost doll
    In JavaScript: event.target is the deepest clicked span
    In real life: Each outer doll feels it in turn
    In JavaScript: Each ancestor listener runs with its own currentTarget
    In real life: The outer doll is not the poke point
    In JavaScript: currentTarget changes; target stays stable

    Where the analogy stops: Real dolls do not have two phases. The analogy is only about one poke being noticed by containers around the poked thing.

    Two event properties people mix up
    PropertyMeaningIn the star-click demo
    event.targetOriginal element where the event beganthe span with the star
    event.currentTargetElement whose listener is currently runningsection, card, toolbar, or button depending on the log line
    event.composedPath()Array-like path the event travelsstarts near the span and includes ancestors
    Professional habit

    Inside a listener, prefer event.currentTarget when you mean “the element I attached this listener to.” Use event.target when you intentionally care about the descendant that was clicked.

    stopPropagation and stopImmediatePropagation

    COMPARE

    Sometimes a listener should stop the trip. A dialog might catch an inside click so a document-level outside-click handler does not close it. That is what event.stopPropagation() does: it stops the event from reaching further nodes. It does not stop other listeners already waiting on the current node.

    Real-life analogyClosing the stairwell door

    Calling stopPropagation() is like closing the stairwell door after news reaches one floor. The news does not travel farther up or down the stairs, but people already in that room can still hear it. stopImmediatePropagation() is the stricter version: it silences the remaining people in the same room too.

    In real life: Close the stairwell door
    In JavaScript: stopPropagation() stops the news going to other floors
    In real life: People in the same room still hear it
    In JavaScript: Other listeners on the same node still run
    In real life: Silence everyone in the room too
    In JavaScript: stopImmediatePropagation() skips remaining same-node listeners

    Where the analogy stops: Events are not sound waves. The methods affect listener delivery only; they do not undo work that already ran and do not prevent default browser actions.

    Three methods with similar names
    MethodStops later nodes?Stops same-node listeners?Stops default action?
    stopPropagation()YesNoNo
    stopImmediatePropagation()YesYesNo
    preventDefault()NoNoYes, when the event is cancelable
    Do not overuse it

    Propagation is how outside-click handlers, analytics, keyboard shortcuts, and the next lesson’s Event delegation pattern work. Stop it only when you own both sides of the behavior and can explain what ancestor should not hear the event.

    Events that do not bubble

    REAL BROWSER

    Capture can observe any event that is dispatched through the tree, but only events with bubbles: true bubble. Useful pairs to remember are focus/blur versus focusin/focusout, and mouseenter/mouseleave versus mouseover/mouseout.

    Which browser events bubble?
    Ancestor listenersPop out in the code editor (opens in a new tab)JavaScript
    panel.addEventListener("focus", log, true);panel.addEventListener("focus", log);panel.addEventListener("focusin", log); panel.addEventListener("mouseenter", log, true);panel.addEventListener("mouseenter", log);panel.addEventListener("mouseover", log); function log(event) {  console.log(event.type, event.bubbles);}
    Try focus and pointer events0 logs
      Try it yourself

      Focus the input, tab away, and move the pointer through the card. Watch event.bubbles: capture can observe non-bubbling events, but ordinary bubble listeners on ancestors only hear bubbling ones.

      This demo reads the browser's real event.bubbles property.
      Does this event bubble?
      • click
      • focusin
      • mouseover
      • focus
      • blur
      • mouseenter
      • mouseleave
      • focusout
      Try it yourself
      0 of 8 correct

      Sort each event by the propagation behavior you usually rely on. The demo above lets you verify the browser's bubbles flag.

      Choose a category for every card. You can change an answer at any time; Reset clears them all.

      Where you will use this

      Propagation is not trivia. It is the reason one listener can manage a whole region, and the reason a component can accidentally interfere with the page around it.

      A careful outside-click patternPop out in the code editor (opens in a new tab)JavaScript
      const menu = document.querySelector(".menu");const button = document.querySelector(".menu-button"); document.addEventListener("click", (event) => {  const clickedInside = menu.contains(event.target) || button.contains(event.target);  if (!clickedInside) menu.hidden = true;}); button.addEventListener("click", (event) => {  event.stopPropagation();  menu.hidden = !menu.hidden;});

      The document listener relies on bubbling so outside clicks arrive. The button stops its own click because the button is not an outside click. In a larger app, you might avoid stopping and instead make the document listener check the path with composedPath().

      In what order do these logs appear?
      • section capture
      • card capture
      • button capture
      • button bubble
      • toolbar bubble
      • section bubble
      Try it yourself
      0 of 6 correct

      Place each listener in the early capture group, at-target group, or later bubble group.

      Choose a category for every card. You can change an answer at any time; Reset clears them all.

      Common misconceptions

      “Bubbling means the event object is copied.”

      One event object travels. Its currentTarget and eventPhase change as different listeners run.

      “Capture only happens for bubbling events.”

      Capture can happen for dispatched events even when bubbles is false. The focus demo proves it.

      “stopPropagation stops everything.”

      It stops later nodes, not other listeners on the current node, and not default actions.

      “target and currentTarget are usually interchangeable.”

      They are equal only when the listener is running on the original target. Ancestor listeners see different values.

      “If a parent hears a child click, the child’s handler failed.”

      No. That is normal bubbling. It is often exactly what you want.

      Practice: trace the trip

      5 EXERCISES
      Exercise 1 · Warm-upName the bubbling phase

      What number does event.eventPhase report during the bubble phase?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 2 · PracticeFind the original target

        If a learner clicks the icon <span> inside a button, what is event.target?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 3 · PracticePredict the first log

          For a button click, these logs represent the order. What is the first log?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log("section capture");
          console.log("button target");
          console.log("section bubble");

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 4 · PracticeChoose the right stop method

            Which method stops the event from reaching later nodes but still lets other listeners on the current node run?

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 5 · ChallengeFix the menu bug

              A teammate used stopPropagation() to stop a link from navigating. What method should they use instead?

              Starter codePop out in the code editor (opens in a new tab)JavaScript
              document.addEventListener("click", closeMenu);
              menu.addEventListener("click", (event) => {
                event.stopPropagation();
              });

              Answer, then press Check. Spacing and letter case don’t matter.

                Quiz: check your understanding

                7 QUESTIONS

                Try each question once before reading the explanations.

                Lesson quiz · 7 questionsScore: first tries count
                1. Question 1 of 7Which phase usually runs first for a click on a button inside a card?

                  Choose an answer to see the explanation.

                2. Question 2 of 7What does this print?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  console.log("section capture");
                  console.log("button target");
                  console.log("section bubble");

                  Choose an answer to see the explanation.

                3. Question 3 of 7You click a span inside a button. In a listener attached to the button, what is event.currentTarget?

                  Choose an answer to see the explanation.

                4. Question 4 of 7Which event object property is 1 during capture, 2 at target, and 3 during bubble?

                  Choose an answer to see the explanation.

                5. Question 5 of 7What does stopPropagation() do?

                  Choose an answer to see the explanation.

                6. Question 6 of 7Which pair gives you bubbling focus events instead of non-bubbling ones?

                  Choose an answer to see the explanation.

                7. Question 7 of 7Why avoid sprinkling stopPropagation() everywhere?

                  Choose an answer to see the explanation.

                Key takeaways

                • Events travel in capture, target, then bubble phases.
                • event.target is the original hit; event.currentTarget is the listener’s element.
                • eventPhase is 1 for capture, 2 at target, and 3 for bubble; composedPath() shows the path.
                • stopPropagation() stops later nodes; stopImmediatePropagation() also skips same-node listeners; preventDefault() stops default actions.
                • Not every event bubbles. Use bubbling alternatives such as focusin/focusout and mouseover/mouseout when an ancestor must hear them.

                Remember the one-liner.
                Propagation is one event object taking a round trip through the tree: capture in, target, bubble out.

                Up next: Event delegation, where you intentionally use bubbling to make one listener handle many elements.

                CompleteFrontend Clear concepts. Working examples.