Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Event bubbling lets an event that starts on a nested element travel up through its ancestors, so a parent can respond to clicks on its children. Event delegation uses that behavior: attach one listener to a shared parent, then identify which descendant was activated. It is a practical pattern for repeated controls and dynamically added content—but it depends on the event bubbling to that parent.

What event bubbling means

Browser events are provided by the Web Platform, rather than being a feature of the JavaScript language itself. When an event propagates through the DOM, it moves through three phases: capture travels from ancestors toward the target, the target phase runs listeners on the target, and bubbling travels back up through ancestors. MDN’s event bubbling guide walks through these phases.

For example, if a button is inside a list, a click on the button can also reach a listener on the list. With addEventListener(), the default is to listen during the bubbling phase, unless you set the capture option to true. See MDN’s addEventListener() reference for the option and its default.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How event delegation works

Instead of attaching a separate listener to every repeated control, delegation attaches a listener to their shared ancestor. The handler checks the event’s target to find the relevant descendant and then runs the appropriate action. This can also cover descendants added after the listener was attached, provided their event bubbles to the delegated ancestor.

Here is a todo-list example. The selector finds the button even when the click lands on an icon or span inside it; the containment check keeps the match inside this list.

const list = document.querySelector(".todo-list");

list.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-action]");

  if (!button || !list.contains(button)) return;

  if (button.dataset.action === "remove") {
    button.closest("li")?.remove();
  }
});

Adapt the selectors and action to your markup. The list element must exist when the listener is attached, and the click event must bubble to it.

Choose the right event target

event.target is where the event originated; event.currentTarget is the object whose listener is currently running. In a delegated handler, the target may be a nested icon rather than the button you want to act on. Use a selector such as closest() to find the intended control, and constrain the match to the delegation root when needed. Use currentTarget when you need to refer to the root whose listener is executing. MDN documents these properties in its Event reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Delegation or a listener on each element?

Neither approach is automatically best. Choose based on the structure and lifecycle of the controls, and on which approach keeps the behavior easiest to understand.

Approach Works well when Considerations
Event delegation Many repeated descendants share a parent, or new descendants need the same behavior after insertion. The event must bubble to the delegated ancestor, and the handler must reliably identify the intended descendant.
Direct listeners A control is small, isolated, or easier to understand with its behavior attached directly. Each relevant element needs its own listener, including elements added later if they need the behavior.

Delegation can centralize code, but the cited documentation does not establish that it is always faster. Prefer the clearest design for the number and lifecycle of your controls.

When to use capture, bubbling, and propagation controls

Use capture when a handler must run on the way down

Set { capture: true } in the addEventListener() options when the listener needs to run during capture, before the event reaches its target. For typical delegated interactions, the default bubbling listener is usually the relevant choice. The timing behavior is described in MDN’s addEventListener() documentation.

Do not confuse stopping propagation with canceling an action

stopPropagation() prevents the event from continuing to propagate, but it does not cancel a browser default action such as following a link. If the event is cancelable and you need to prevent its default action, call preventDefault(). stopImmediatePropagation() also prevents other listeners on the same element from running. MDN explains these distinctions in the stopPropagation() reference and Event reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Avoid stopping propagation as a routine fix for an unexpected handler: doing so can keep unrelated ancestor listeners from receiving the event.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Events and Shadow DOM boundaries

Delegation across a Shadow DOM boundary needs more than a listener on an outer ancestor. The event must be both bubbling and composed to propagate through that boundary. For custom events, check their bubbles and composed settings; neither behavior should be assumed. MDN describes boundary crossing in its Event.composed reference.

event.composedPath() can help inspect the route an event took. However, a path observed outside a closed shadow root does not reveal that root’s internal nodes. The Event reference documents composedPath() and related event properties.

A quick debugging checklist

  • Confirm the event actually bubbles to the ancestor where the listener is attached.
  • Check whether the listener is using the default bubbling phase or was registered with capture enabled.
  • Inspect event.target and event.currentTarget; the target may be a nested element inside the control.
  • For Shadow DOM, check both bubbles and composed, and use composedPath() with closed-root visibility in mind.
  • When behavior seems canceled, distinguish propagation from the browser’s default action: stopPropagation() and preventDefault() do different jobs.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.