JavaScript event bubbling: DOM propagation and delegation

Event bubbling: inner-to-outer delivery, delegation on a parent, stopPropagation vs preventDefault, and capture phase contrast. Snippets use `{run=false}`—run in DevTools or with jsdom/happy-dom where markup exists.

Published

Updated

Read time 4 min read

Reviewed byDeepak Prasad

JavaScript event bubbling: DOM propagation and delegation

Readers land on this topic through many short phrases: bubbling javascript, event bubbling in javascript, bubble in javascript, or simply event bubbling. They all describe the same default: after a target element handles an event, the same event object can travel outward through ancestor nodes unless something stops it. That outward phase is what people mean by javascript event bubbling or js event bubble.

Pairing a listener on a parent is how event delegation works—you listen once and inspect event.target for many children. For more listener basics, see addEventListener in JavaScript and the localStorage event handler example.

The snippets below are excerpts (they assume #container, #paragraph, #inner, #child, #wrap, #link, etc.). The public Run control has no DOM—use {run=false} and run in a browser or with happy-dom / jsdom after injecting the matching HTML.

Tested on: Node.js v20.18.2 with happy-dom for editorial QA where snippets are completed with markup. Notes describe expected log order.


Quick reference

Bubbling = target first, then ancestors; stopPropagation stops that walk; preventDefault affects default actions, not necessarily bubbling.

Goal Tool
Stop ancestors hearing the event event.stopPropagation()
Cancel link navigation / form submit (if cancelable) event.preventDefault()
Listen for many children cheaply One listener on parent + check event.target
Run before target (intercept) addEventListener(..., true) capture

What is event bubbling in JavaScript?

When you call addEventListener without a third argument, the handler runs in the bubbling phase by default. After the event reaches its target, it walks up the tree toward document (and beyond in modern browsers), firing compatible listeners on each ancestor. That is why a click on a nested button can also satisfy a click listener registered on the surrounding card or layout region.


Bubbling order from an inner element

Diagram: nested elements with an upward arrow showing the bubble path from inner target to outer ancestors

Assume markup with a paragraph inside a container. Register two bubble-phase listeners, then dispatch a bubbled click on the paragraph. The inner handler runs at the target, then the outer handler runs as the event propagates up.

javascript
document.getElementById("container").addEventListener("click", () => {
  console.log("container");
});
document.getElementById("paragraph").addEventListener("click", () => {
  console.log("paragraph");
});
// synthetic click on #paragraph with bubbles: true

You should see logs in order: paragraph, then container.

In a real page you would click with the mouse; automated tests use a synthetic MouseEvent so the log order stays stable.


Delegation: one listener, many descendants

Because bubbling is reliable for most DOM events, you can attach a single listener on #container and still react to clicks on children such as #inner. The event reaches the parent after bubbling up from the child.

javascript
document.getElementById("container").addEventListener("click", () => {
  console.log("delegated");
});
// synthetic click on #inner with bubbles: true

You should see one line: delegated.

This pattern scales when lists or tables render many rows and you do not want one listener per cell.


Stopping propagation with stopPropagation

event.stopPropagation() tells the engine to skip further propagation steps for this event object. It does not automatically cancel default actions (such as following a link); it only affects who else hears the event.

javascript
document.getElementById("child").addEventListener("click", (e) => {
  e.stopPropagation();
  console.log("child");
});
document.getElementById("wrap").addEventListener("click", () => {
  console.log("wrap");
});
// synthetic click on #child with bubbles: true

You should see one line: child.

The parent handler never runs because the child stopped bubbling.


preventDefault does not turn off bubbling

A common mix-up is to treat preventDefault() like a bubbling switch. It is not. preventDefault() cancels the browser’s default action for cancelable events (for example navigation on an <a href>), but the event can still bubble unless you also call stopPropagation() or stopImmediatePropagation().

javascript
const link = document.getElementById("link");
let bodyHeard = false;
document.body.addEventListener("click", () => {
  bodyHeard = true;
});
link.addEventListener("click", (e) => {
  e.preventDefault();
  console.log("link click");
});
// synthetic cancelable click on the anchor with bubbles: true
console.log("body bubble:", bodyHeard);

You should see two lines: link click, then body bubble: true.

So default may be suppressed while ancestors still observe the click—design your handlers with that in mind.


Capturing phase (optional contrast)

addEventListener(type, handler, true) registers a capturing listener that runs on the way down toward the target, before bubbling listeners on the same node. Most UI code relies on bubbling; capturing is useful for intercepting or instrumentation layers.


Summary

Bubbling is the default upward phase after the target; delegation leans on that path; stopPropagation and preventDefault solve different problems—combine them deliberately when you need both.

Bubbling in javascript is the default second phase of DOM event delivery: after the target handlers run, the event walks outward through ancestors unless propagation is stopped. Event delegation relies on that model—one listener on a parent can service many children—while stopPropagation answers the FAQ “how do I keep parent handlers from firing?” without touching default actions.

preventDefault is the answer to a different question (canceling defaults such as link navigation or form submit), so mixing it up with bubbling is a common debugging mistake. Use capture-phase listeners sparingly when you truly need to intercept events on the way down, and prefer platform observers when you only need visibility or geometry instead of hammering scroll with synchronous layout reads.


References

Olorunfemi Akinlua

Boasting over five years of experience in JavaScript, specializing in technical content writing and UX design. With a keen focus on programming languages, he crafts compelling content and designs …