Bubbling, Capturing & Delegation

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

When you click a button inside a card inside a panel, the browser does not treat that as a button-only moment. The event travels through the DOM tree, and that travel explains why parent listeners can hear child clicks, why target and currentTarget differ, and why one listener can manage a whole list.

This lesson gives that travel a map: capture → target → bubble. Once you can see the path, stopPropagation, preventDefault, and event delegation stop feeling like magic words.

Events move through phases

Most browser events that start on an element move through three phases:

  1. Capture phase — the event travels from the top of the document down toward the target.
  2. Target phase — the event reaches the element where it started.
  3. Bubble phase — the event travels back up through the target's ancestors.

Every dispatched event has a phase, but not every event bubbles all the way back up. Click events do, and they are the right first model because most UI actions you wire by hand are click-like.

By default, addEventListener("click", handler) listens during the bubble phase. Add { capture: true } when you need a listener to run on the way down instead.

Click Approve request and watch the ordered log. The same click is heard by several listeners because the button sits inside the card, and the card sits inside the board.

web page — live previewsandboxed
Run rebuilds the page

The browser is not duplicating the click. It is delivering the same event object along a path. Each listener sees the same event.target — the button where the click started — but a different event.currentTarget, the element whose listener is running at that moment.

Stopping travel is not canceling behavior

Two event methods are easy to mix up:

  • event.stopPropagation() stops the event from traveling to more listeners on other elements.
  • event.preventDefault() asks the browser not to perform the event's default action.

A <summary> inside <details> has a default action: clicking the summary toggles the details open and closed. That gives us a default behavior you can see without leaving the preview.

web page — live previewsandboxed
Run rebuilds the page

Click Prevent default only first. The parent listener still hears the click because propagation was not stopped, but that details panel stays closed. Then click Stop propagation only. The parent line does not change, but the details panel opens because the summary's default action was not canceled.

Forms use preventDefault() all the time, but they deserve their own careful lesson. For now, separate the questions: "Should other listeners hear this event?" and "Should the browser do its default behavior?"

Delegation uses one listener for many children

Event delegation means putting one listener on an ancestor and using event.target plus closest(...) to figure out which child action happened.

This works because bubbling brings child events up to the ancestor. It also works for future elements, because the listener is attached to the stable parent, not to each button that exists right now.

web page — live previewsandboxed
Run rebuilds the page

The key line is event.target.closest("button[data-action]"). A user might click the text inside a button, an icon inside a button, or the button itself. closest climbs from the exact target to the useful action button. Then button.closest(".ticket") finds the row that action belongs to.

Delegation is not automatically better for every tiny page. It shines when many similar children share behavior, when rows can be added or removed, or when a stable parent naturally owns the behavior. The parent listener becomes the little router: inspect the clicked action, find the local row, update that row.

You now know how events move, how to stop travel without confusing it with default behavior, and how to let bubbling reduce listener wiring. Next, forms bring the browser's built-in input behavior into the story: reading values, handling submit, and using preventDefault() for a real form flow.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion