How Bubbling Works
Events Fire on the Target First
The element that was actually clicked is called event.target. The event fires there first, then travels upward through every ancestor.
Every Ancestor Gets the Event
After the target, the event fires on the parent, grandparent, and so on up to document and window. Any element with a matching listener along the way will respond.
Bubbling Is Default Behavior
Most DOM events bubble. Some do not (focus, blur). You do not need to do anything to make bubbling happen — it is always on unless you stop it.
The Bubbling Path
// Click on <span class="robot-name"> inside a .robot-card
// The event travels up:
//
// span.robot-name ← event fires here first (event.target)
// div.robot-card
// div.card-container ← delegated listener lives here
// main
// body
// html
// document
// window
//
// Any element with a matching listener along the path fires it. The Delegation Pattern
Listen on a Parent, Not Every Child
Add one listener to the container. Any child that is clicked bubbles the event up to the container. The container's handler runs for all of them.
Dynamic Children Work for Free
Elements added to the container after the listener was wired still bubble events up to it. No additional listeners needed when adding cards dynamically.
Use .closest() to Identify the Card
event.target.closest('.robot-card') finds the nearest ancestor matching the selector, starting from whatever element was actually clicked. Returns null if none found.
Always Guard Against Null
After .closest(), check the result before using it: if (!card) return;. Clicks on container padding or gaps between cards return null.
Delegation Pattern
// The delegation pattern
const container = document.querySelector('.card-container');
container.addEventListener('click', function(event) {
// Find the nearest .robot-card ancestor of what was clicked
const card = event.target.closest('.robot-card');
// Guard: if click missed all cards, do nothing
if (!card) return;
// Work with the card
console.log(card.dataset.robotId);
}); The .closest() Pattern
// .closest(selector) walks UP the DOM tree
// Returns the nearest ancestor matching selector
// Returns null if no match is found
// Starting from event.target (the clicked element):
const card = event.target.closest('.robot-card');
// Works whether the user clicked:
// - the .robot-card div itself
// - a .robot-name span inside it
// - an icon or image inside that span
// All three bubble up through .robot-card. event.target vs. event.currentTarget
// event.target vs. event.currentTarget in delegation
container.addEventListener('click', function(event) {
console.log(event.target);
// The element actually clicked — could be a span, icon, etc.
// This changes with every click.
console.log(event.currentTarget);
// Always the element the listener is attached to — the container.
// This never changes.
}); Dynamic Cards with Delegation
// Dynamic cards work automatically with delegation
// — no extra listener needed
const newCard = document.createElement('div');
newCard.classList.add('robot-card'); // delegation targets this class
newCard.dataset.robotId = 'unit-99'; // data your handler reads
newCard.innerHTML = '<span class="robot-name">Unit 99</span>';
container.appendChild(newCard);
// Clicking newCard bubbles up to the container listener. ✓ Stopping Propagation
// Stopping bubbling (use carefully)
innerButton.addEventListener('click', function(event) {
event.stopPropagation();
// This event will NOT bubble up to parent listeners.
// Useful when a child has a different action than the parent.
// Overusing this breaks delegation — parent listeners never fire.
});