completefrontend
Open lab My progress
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
    Follow the tripSay in what order capture, target, and bubble listeners run when you click an element inside other elements.
  • 02
    Read the event objectUse target, currentTarget, eventPhase, and composedPath() to see where an event started and where it is now.
  • 03
    Stop with carePick the right one of stopPropagation, stopImmediatePropagation, and preventDefault.

The trip an event takes

On a web page, elements sit inside other elements. A button can sit inside a card. The card can sit inside a section. The elements around the button (its parent, the parent's parent, and so on) are called its ancestors.

When you click the button, the browser does not tell only the button. It makes one event object: an object that describes the click. Then it carries that object on a trip through the button's ancestors. The trip has three parts, called phases:

  1. Capture: the event goes in, from the outside down to the button.
  2. Target: the event reaches the button, the element you actually clicked.
  3. Bubble: the event goes back out, from the button up to the outside.

A listener is a function you add with addEventListener. Any element on the trip can have listeners, so any of them can react to the click. This explains three everyday things:

  • A card can hear a click on a button inside it.
  • A menu can close when you click anywhere else on the page.
  • One call to stopPropagation() can break code far away from that line.
Real-life analogyA package delivered in an apartment building

A courier brings a package to an apartment. The courier walks in through the lobby, goes up to the right floor, and reaches the apartment. After the delivery, the news travels back the other way: apartment, then floor, then lobby. A click takes the same kind of trip through elements that sit inside each other.

In real life: The courier walks in through the lobby
In JavaScript: Capture starts on the outside: window, document, then the section
In real life: The courier reaches the apartment
In JavaScript: The target phase, on the clicked element
In real life: The news goes back to the floor and the lobby
In JavaScript: The bubble phase goes back out through the ancestors
In real life: Anyone on the way can react
In JavaScript: Any ancestor with a listener can run code
The main idea

Event propagation is the trip one event object takes. It goes in through the ancestors to the clicked element. Then it goes back out through the same ancestors.

Capture, target, and bubble phases

STEP THROUGH

You choose which half of the trip a listener joins. Pass { capture: true } as the third argument to addEventListener, and the listener runs on the way in. Leave the third argument out, and it runs at the target or on the way out.

Listeners for the way in and the way outPop 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);

Lines 1 to 3 add capture listeners, so they run on the way in. Lines 5 to 7 leave the option out, so they run on the way out. Each line adds the same log function to one element.

Three elements stacked: section on top, card in the middle, button at the bottom. An arrow on the left goes down from section to button: 1, capture, goes in. The button is 2, the target. An arrow on the right goes up from button to section: 3, bubble, goes out.sectioncardbutton1. capturegoes in3. bubblegoes out2. target
A click on the button first goes in, from section down to the button. Then it goes back out, from the button up to section.
One capture listener and one normal listenerPop out in the code editor (opens in a new tab)JavaScript
const card = document.querySelector("[data-box='card']"); card.addEventListener("click", () => console.log("capture"), { capture: true });card.addEventListener("click", () => console.log("bubble"));document.querySelector("[data-box='button']").click();

This example prints capture, then bubble. Line 3 passes { capture: true }, so its listener runs on the way in. Line 4 leaves the option out, so its listener runs on the way out. Line 5 clicks the button inside the card.

Older code writes true as the third argument instead. It means the same thing. The object form is easier to read, and it can hold other options too, such as once.

Guess the order of a nested clickPop out in the code editor (opens in a new tab)JavaScript
const order = [];const card = document.querySelector("[data-box='card']");const button = document.querySelector("[data-box='button']"); card.addEventListener("click", () => order.push("card capture"), { capture: true });button.addEventListener("click", () => order.push("button target"));card.addEventListener("click", () => order.push("card bubble"));button.click();console.log(order.join(" > "));

This prints card capture > button target > card bubble. Line 5 runs first, because capture goes in from the outside. Line 6 runs next, on the button, the target. Line 7 runs last, when the event bubbles back out to the card. To guess the order, draw the trip first.

The full order for a click on the button is:

  1. Capture listeners run from the outside in.
  2. The button's own listeners run. Its capture listeners run before its normal ones in today's browsers. Older browsers did not always agree, so don't write code that needs this exact order.
  3. Normal listeners run from the inside out. This only happens if the event's bubbles property is true.

The table lists the three phases. Read each row as: "in this phase, the event goes this way, eventPhase is this number, and this is how you listen."

The three phases
PhaseWhich way does it go?What is eventPhase?How do you listen?
CaptureIn: window, then document, then each ancestor down to the parent.1, also called CAPTURING_PHASE.Pass { capture: true } or true.
TargetIt stays on the clicked element.2, also called AT_TARGET.Both kinds run. Capture listeners go first in today's browsers.
BubbleOut: the parent, then each ancestor up to document, then window.3, also called BUBBLING_PHASE.Leave the option out. It only runs if event.bubbles is true.

The step-through below does not use a real page. It is a small plain-JavaScript copy of the trip, with a section, a card, a toolbar, a button, and an icon inside the button. Step through it to see the order. Then compare it with the real browser in the next section.

Step through the trip
Step 0 of 11Ready
Your turn: follow the blue line

That is the whole trip. Pick a different stop above and step through again, then compare with the real page in the next section.

Running in
  1. script
Next: line 5
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");}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Where should the trip stop?

This step-through is a small plain-JavaScript copy of the trip. The demo in the next section uses the real browser.

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 the trip in a real page

INTERACTIVE

Now try the browser's real events. The demo shows four boxes, one inside the other: a section, a card, a toolbar, and a button with a star in the middle. Each box has two bells:

  • ↓ in rings when the box's capture listener runs, on the way in.
  • ↑ out rings when the box's normal listener runs, on the way out.

Click the star and watch the bells ring in order. The numbers on the bells, and the row under the boxes, show the order.

Watch the click travel: click the star
↓ in: rings on the way in (capture)↑ out: rings on the way out (bubble)stop: calls stopPropagation()
section
card
toolbar
button
Try it yourself

Click the star. The click travels in through each box, from the outside to the star, then back out again. Each bell rings when its box's listener runs.

These are real browser listeners. Tap a bell to switch it: on → stop sign → off.

Tap a bell to turn it off, or tap it again to give it a stop sign. A stop sign means that listener calls event.stopPropagation(). Then click the star again: every bell after the stop sign stays silent.

Game: guess the order

Now test yourself. Each level turns on a few bells. Tap them in the order you think they will ring, then click the star. The real click checks your guess. Get it right on the first try for three stars.

Game: guess the order the bells ring
↓ in: rings on the way in (capture)↑ out: rings on the way out (bubble)stop: calls stopPropagation()
section
card
toolbar
button
Step 1 of 6★ 0
Level 1: Warm-up

The section listens both ways. Tap the bells in the order you think they'll ring. Then click the star to send a real click.

Each level uses real browser listeners. Clear a level on the first try for three stars.

The demo and the game both add real listeners, like this:

What the demo addsPop out in the code editor (opens in a new tab)JavaScript
const boxes = document.querySelectorAll("[data-box]"); for (const box of boxes) {  box.addEventListener("click", ringIn, { capture: true });  box.addEventListener("click", ringOut);} function ringIn(event) {  console.log(event.currentTarget.dataset.box, "in");} function ringOut(event) {  console.log(event.currentTarget.dataset.box, "out");}

target vs currentTarget

The event object has two properties that are easy to mix up:

  • event.target is the element where the event started: the deepest element you clicked.
  • event.currentTarget is the element whose listener is running right now.

In the demo, click the star. The star is a span inside the button. In the section's log line, target is still the span, but currentTarget is the section.

The two properties can be differentPop out in the code editor (opens in a new tab)JavaScript
const card = document.querySelector("[data-box='card']"); card.addEventListener("click", (event) => {  console.log(event.target.dataset.box);  console.log(event.currentTarget.dataset.box);}); document.querySelector("[data-box='button']").click();

This prints button, then card. Line 8 clicks the button, so the button is the target. The listener belongs to the card, so the card is the currentTarget. As the event moves on, currentTarget changes to each new element. target stays the same.

The card is above the button. An arrow goes up from the button to the card. Next to the card: currentTarget = card, listener runs here. Next to the button: target = button, click starts here.cardbuttoncurrentTarget = cardlistener runs heretarget = buttonclick starts here
The click starts on the button and bubbles up to the card. In the card's listener, target is the button and currentTarget is the card.
Real-life analogyNesting dolls feel the poke

Picture a set of nesting dolls, one inside the other. Poke the smallest doll in the middle, and every doll around it feels the poke. The spot you poked stays the same. The doll that is feeling it right now keeps changing.

In real life: You poke the smallest doll
In JavaScript: event.target is the clicked span
In real life: Each bigger doll feels it in turn
In JavaScript: Each ancestor's listener runs, with its own currentTarget
In real life: The spot you poked never moves
In JavaScript: target stays the same; currentTarget changes

The table compares three things you can read from the event. Read each row as: "this property tells you this, and in the star demo it is this."

Where it started, who is listening, and the full trip
What you readWhat does it tell you?What is it when you click the star?
event.targetThe element where the event started.It is the span with the star, every time.
event.currentTargetThe element whose listener is running now.It is the section, card, toolbar, or button, depending on the log line.
event.composedPath()The list of elements on the event's trip.It starts at the span and lists each ancestor after it.
Print the first steps of the tripPop out in the code editor (opens in a new tab)JavaScript
const button = document.querySelector("[data-box='button']"); button.addEventListener("click", (event) => {  console.log(event.composedPath().slice(0, 3).map((node) => node.dataset.box || node.tagName).join(" > "));}); button.click();

This prints button > toolbar > card. composedPath() returns an array of the elements the event passes through. It starts at the target and goes outward. Line 4 keeps the first three and prints their data-box names.

It also works with shadow DOM, a way for a component to hide its inner elements. There, walking up with parentElement can miss steps, but composedPath() still lists the whole trip.

A good habit

Inside a listener, use event.currentTarget when you mean "the element I added this listener to." Use event.target only when you need the inner element that was clicked.

stopPropagation and stopImmediatePropagation

COMPARE

Sometimes a listener needs to end the trip. For example, a dialog box may want to keep its own clicks away from a page listener that closes it. event.stopPropagation() does this: the event does not go on to any more elements.

It does not stop other listeners on the same element. They still run.

Real-life analogyClosing the stairwell door

News reaches one floor of a building. You close the stairwell door, so the news can't travel to other floors. But the people already in that room can still hear it. To keep them from hearing it too, you would also have to ask the room to stay quiet.

In real life: Close the stairwell door
In JavaScript: stopPropagation() keeps the event from other elements
In real life: People in the same room still hear the news
In JavaScript: Other listeners on the same element still run
In real life: Also ask the room to stay quiet
In JavaScript: stopImmediatePropagation() skips the rest of this element's listeners

Three methods have similar names. The table shows what each one stops. Read each row as: "this method stops these things, and not the others."

What each method stops
MethodDoes it stop later elements?Does it stop other listeners on this element?Does it stop the browser's built-in action?
stopPropagation()Yes.No.No.
stopImmediatePropagation()Yes.Yes.No.
preventDefault()No.No.Yes, if the event allows it to be cancelled.

The browser's built-in action is what it does on its own, such as following a link or sending a form. Only preventDefault() stops that.

Immediate also stops this element's other listenersPop out in the code editor (opens in a new tab)JavaScript
const button = document.querySelector("[data-box='button']");const log = []; button.addEventListener("click", (event) => {  log.push("first");  event.stopImmediatePropagation();});button.addEventListener("click", () => log.push("second"));button.click();console.log(log.join(" > "));

This prints only first. The button has two listeners. The first one calls stopImmediatePropagation() on line 6. So the second listener, on line 8, is skipped, and no ancestor hears the click. If line 6 called stopPropagation() instead, both button listeners would run and it would print first > second. The ancestors would still hear nothing.

Left: stopPropagation(). The button holds two listeners, first and second. Both run, so it prints first > second, and the card is skipped. Right: stopImmediatePropagation(). The same button prints only first. The second listener and the card are skipped.stopPropagation()firstsecondbuttonprints first > secondcard: skippedstopImmediatePropagation()firstsecondbuttonprints firstsecond, card: skipped
Both methods stop the click from reaching the card. Only stopImmediatePropagation() also skips the second listener on the same button.
Don't stop the trip often

Many features need to hear clicks from inside other elements: menus that close when you click outside, analytics that count clicks, and keyboard shortcuts. The next lesson, Event delegation, also depends on the trip. Stop it only when you know exactly which ancestor must not hear the event, and why.

Events that do not bubble

REAL BROWSER

Every event goes through the capture phase. But only events whose bubbles property is true go back out. Some common events don't bubble, and each has a partner that does:

  • focus and blur don't bubble. focusin and focusout do.
  • mouseenter and mouseleave don't bubble. mouseover and mouseout do.
Which browser events bubble?
Listeners on the outer boxPop 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

    Click the field, press Tab to leave it, and move the pointer over the button. Each log shows event.bubbles. Capture listeners on the box hear events from the elements inside it. Normal listeners on the box hear them only if they bubble.

    The log shows the browser's real event.bubbles value.
    focus has a partner that bubblesPop out in the code editor (opens in a new tab)JavaScript
    const card = document.querySelector("[data-box='card']");const button = document.querySelector("[data-box='button']");const log = []; card.addEventListener("focus", () => log.push("focus capture"), true);card.addEventListener("focus", () => log.push("focus bubble"));card.addEventListener("focusin", () => log.push("focusin bubble"));button.focus();console.log(log.join(" > "));

    This prints focus capture > focusin bubble. Line 8 puts the focus on the button inside the card.

    • Line 5 is a capture listener, so it hears focus on the way in.
    • Line 6 is a normal listener for focus. It never runs, because focus does not bubble.
    • Line 7 listens for focusin, which bubbles, so it runs.

    So when a form wrapper needs to know that focus went into one of its fields, listen for focusin and focusout.

    Left: focus. The button gets focus, but no arrow goes up to the card: does not bubble. Right: focusin. An arrow goes up from the button to the card: bubbles to card.focuscardbuttondoes not bubblefocusincardbuttonbubbles to card
    focus stays on the button, so the card's normal listener never runs. focusin bubbles up, so the card hears it.
    mouseenter does not bubble, mouseover doesPop out in the code editor (opens in a new tab)JavaScript
    const card = document.querySelector("[data-box='card']");const button = document.querySelector("[data-box='button']");const log = [];const record = (event) => log.push(event.type + " " + event.bubbles); card.addEventListener("mouseenter", record, true);card.addEventListener("mouseenter", record);card.addEventListener("mouseover", record);button.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false }));button.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));console.log(log.join(" > "));

    This prints mouseenter false > mouseover true. Each log shows the event's name and its bubbles value. Lines 9 and 10 send the two events to the button.

    • Line 6 is a capture listener, so it hears mouseenter on the way in.
    • Line 7 is a normal listener for mouseenter. It never runs, because mouseenter does not bubble.
    • Line 8 listens for mouseover, which bubbles, so it runs.

    Use mouseover when an ancestor should hear the pointer moving onto its inner elements. Use mouseenter when you want one event per element, with no bubbling.

    Does this event bubble?
    • click
    • focusin
    • mouseover
    • focus
    • blur
    • mouseenter
    • mouseleave
    • focusout
    Try it yourself
    0 of 8 correct

    Sort each event into the right group. Use the demo above to check the browser's bubbles value.

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

    Where you will use this

    You will use the trip in real code. It lets one listener handle a whole area of the page. It is also why one part of a page can block another part by accident.

    Close a menu when you click outside itPop 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;});
    • Lines 4 to 7 add a listener to the whole document. Clicks anywhere bubble up to it.
    • Line 5 checks whether the click was inside the menu or its button. contains() returns true if the element is the same one or sits inside it.
    • Line 6 hides the menu if the click was outside.
    • Line 10 stops the menu button's own click, because a click on the button is not an outside click.

    In a bigger app, you might skip the stop on line 10. Instead, the document listener can check the trip with composedPath().

    A stop can hide clicks from analyticsPop out in the code editor (opens in a new tab)JavaScript
    const log = [];const card = document.querySelector("[data-box='card']");const button = document.querySelector("[data-box='button']"); card.addEventListener("click", () => log.push("analytics"));button.addEventListener("click", (event) => {  event.stopPropagation();  log.push("button");});button.click();console.log(log.join(" > "));

    This prints button, not button > analytics. Line 5 gives the card a listener that records clicks for analytics. But line 7 stops the event at the button, before it can bubble up to the card. So the analytics listener never records the click.

    In a real app, a stop like this can quietly break analytics, keyboard shortcuts, or a menu that closes on outside clicks. When you can, let the event bubble and add a check in the ancestor's listener instead.

    Close a modal when you click outside itPop out in the code editor (opens in a new tab)JavaScript
    document.body.innerHTML = '<button class="open-modal">Open</button><div class="modal" hidden>Details</div>';const modal = document.querySelector(".modal");const openButton = document.querySelector(".open-modal"); document.addEventListener("click", (event) => {  if (!event.composedPath().includes(modal) && !event.composedPath().includes(openButton)) {    modal.hidden = true;  }});openButton.addEventListener("click", () => { modal.hidden = false; });openButton.click();document.body.click();console.log(modal.hidden);

    This prints true, which means the modal is hidden again. A modal is a box that opens on top of the page.

    1. Line 11 clicks the Open button, and its listener on line 10 shows the modal.
    2. That click also bubbles to the document. Line 6 finds the Open button in the trip, so the modal stays open.
    3. Line 12 clicks the page body. That click bubbles to the document listener on line 5.
    4. Line 6 checks the trip. Neither the modal nor the Open button is in it, so line 7 hides the modal.

    This code never stops the trip, so other document listeners still hear every click. In a real app, also close the modal when the user presses Escape, and move the focus back to the Open button.

    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

    The user clicks the button. Put each listener in the right group: before the target, at the target, or after the target.

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

    Common misconceptions

    "Bubbling makes a copy of the event for each element."

    There is only one event object. As it moves, its currentTarget and eventPhase change.

    "Capture only happens for events that bubble."

    Capture happens even when bubbles is false. The focus example shows this: the capture listener ran.

    "stopPropagation stops everything."

    It only stops later elements. Other listeners on the same element still run, and the browser's built-in action still happens.

    "target and currentTarget are the same thing."

    They are the same only in a listener on the clicked element itself. In an ancestor's listener, they are different.

    "If the parent hears a child's click, something went wrong."

    No. That is normal bubbling, and it is often exactly what you want.

    Practice: follow the trip

    6 EXERCISES
    Exercise 1 · Warm-upName the bubble phase number

    What number is event.eventPhase during the bubble phase?

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

      Exercise 2 · PracticeFind where the click started

      You click the icon <span> inside a button. What is event.target?

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

        Exercise 3 · PracticeFind the first log

        These three logs show the order for a click on the button. What does the first one print?

        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 · PracticePick the right stop method

          Which method keeps the event from reaching later elements, but still lets the other listeners on this element run?

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

            Exercise 5 · ChallengeFix the link bug

            A teammate used stopPropagation() to keep a link from opening its page. The link still opens. Which method should they use instead?

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

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

              Exercise 6 · ChallengeKeep analytics working

              A card records analytics in its click listener. A button inside the card opens details, but it calls stopPropagation(). What should you remove so the card still records the click?

              Starter codePop out in the code editor (opens in a new tab)JavaScript
              card.addEventListener("click", recordAnalytics);
              button.addEventListener("click", (event) => {
                event.stopPropagation();
                openDetails();
              });

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

                Quiz: check your understanding

                8 QUESTIONS

                Try each question once before you read the explanations.

                Lesson quiz · 8 questionsScore: first tries count
                1. Question 1 of 8You click a button inside a card. Which listeners run first?

                  Choose an answer to see the explanation.

                2. Question 2 of 8These logs show the order for a click on the button. What is the first line printed?

                  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 8You click a span inside a button. In a listener added to the button, what is event.currentTarget?

                  Choose an answer to see the explanation.

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

                  Choose an answer to see the explanation.

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

                  Choose an answer to see the explanation.

                6. Question 6 of 8Which pair of focus events bubbles?

                  Choose an answer to see the explanation.

                7. Question 7 of 8Why should you not call stopPropagation() everywhere?

                  Choose an answer to see the explanation.

                8. Question 8 of 8What does composedPath() show you?

                  Choose an answer to see the explanation.

                Key takeaways

                • An event takes a trip in three phases: capture (in), target, then bubble (out).
                • event.target is where the event started. event.currentTarget is the element whose listener is running.
                • eventPhase is 1 for capture, 2 at the target, and 3 for bubble. composedPath() lists the whole trip.
                • stopPropagation() stops later elements. stopImmediatePropagation() also skips this element's other listeners. preventDefault() stops the browser's built-in action.
                • Some events don't bubble. When an ancestor must hear them, use the partners that do: focusin/focusout and mouseover/mouseout.

                Remember this.
                One event object takes a trip through the page: in during capture, at the target, then out during bubble.

                Up next: Event delegation. You will use bubbling on purpose, so one listener can handle many elements.

                CompleteFrontend Clear concepts. Working examples.