Frontend Master

JavaScript Curriculum

Bubbling & Delegation
+60 XP

Bubbling & Delegation

medium
~30 min·60 XP

The menu now loads cards dynamically from a server. Attaching a click listener to every card would break whenever new cards arrive. Event delegation lets one listener on the container handle all cards — past and future.

Bubbling & Delegation

When you click a button inside a card inside a section, the click event fires on the button — then bubbles up to the card, then the section, then main, then body, then document. This is event bubbling.

Event Propagation Visualizer

See exactly how events travel through the DOM — click any element and watch the path:

bubbling.js
Click an element to fire the event (bubble phase)
Event path

Click an element above to see the bubble path

// Default: events bubble UP
el.addEventListener('click', handler)
// fires: target → parent → ... → document
ℹ️Capture vs Bubble phase
Events travel in two phases: first DOWN the tree (capture phase), then UP from the target (bubble phase). By default, addEventListener listens in the bubble phase. Pass `true` or `{ capture: true }` as the third argument to listen during capture instead. In practice, bubble phase is almost always what you want.

stopPropagation & preventDefault

Two methods that control event behaviour — they are independent and do different things:

propagation.js

Cancels the browser's default action for the event. Does NOT stop bubbling — the event still propagates.

✓ With it
form.addEventListener('submit', (e) => {
  e.preventDefault()  // stops page reload
  // now handle with JS:
  const data = new FormData(e.target)
  sendToServer(data)
})

link.addEventListener('click', (e) => {
  e.preventDefault()  // stops navigation
  openModal(link.href)
})
✗ Without it
// Without preventDefault — page reloads on submit:
form.addEventListener('submit', (e) => {
  sendToServer(new FormData(e.target))
  // page reloads before fetch completes!
})
preventDefault and stopPropagation are independent. Calling one does not call the other.
⚠️Don't overuse stopPropagation
Stopping propagation makes your code harder to debug and breaks features that rely on events reaching higher elements (like analytics, accessibility tools, and document-level keyboard handlers). Prefer fixing your architecture over stopping propagation.

Event Delegation — One Listener for All

Instead of attaching listeners to every child, attach one to the parent and use e.target to figure out what was clicked:

event-delegation.js
// ✓ Delegation: one listener on parent
container.addEventListener('click', (e) => {
  const card = e.target.closest('[data-card-id]')
  if (!card) return

  if (e.target.matches('.order-action'))
    handleOrder(card.dataset.cardId)

  if (e.target.matches('.remove-action'))
    card.remove()
  // Works for ALL cards — even future ones!
})
Espresso£2.50
Cappuccino£3.50
Pour Over£4.00
// Click buttons inside any card
💡Delegation is the standard pattern for dynamic lists
Any time you render a list from data — menu cards, search results, todo items — use event delegation. It uses less memory, works for dynamically added elements, and keeps your listener code in one place.

Your Challenge

Replace all individual .card click listeners with one delegated listener on .cards. Inside it: use e.target.closest('.card') to find the card, e.target.matches('.order-btn') to detect the order button, and e.target.matches('.remove-btn') to detect remove. Add a new card after wiring — confirm the listener covers it automatically.

Challenge

Replace individual card listeners with a single delegated listener on .cards. Use e.target.closest('.card') to find which card was clicked and e.target.matches('.order-btn') to check if the order button was the target.

bubblingcapturingstopPropagationpreventDefaultevent-delegationevent.targetclosestmatchesdynamic-elements
Bubbling & Delegation | Nexus Learn