In the DOM (Document Object Model), when an event occurs on an element, it travels through the DOM in a specific order. This is known as event propagation. It travels through the DOM in phases.
✅ Three Event Phases
1️⃣ Capturing Phase (Event goes DOWN the tree)
→ From window → document → <html> → <body> → parent elements → target
2️⃣ Target Phase
→ Event reaches the actual clicked element
3️⃣ Bubbling Phase (Event goes UP the tree)
→ From target element → parent → <body> → <html> → document
🔽 Event Capturing (Trickling Phase)
Event is handled from outermost to innermost
Rarely used but available
How to enable capturing:
element.addEventListener('click', handler, true);✅ The true makes the listener run during capturing phase
🔼 Event Bubbling (Default Behavior)
Event is handled from innermost to outermost
Default phase in JavaScript
Bubbling example:
<div id="parent">
<button id="child">Click Me</button>
</div>
<script>
document.getElementById('parent').addEventListener('click', () => {
console.log('Parent clicked');
});
document.getElementById('child').addEventListener('click', () => {
console.log('Child clicked');
});
</script>Child clicked
Parent clicked✋ Stopping the Event Flow
stopPropagation()
Stops event from continuing to bubble or capture.
<div id="parent">
Parent Div
<button id="child">Click Child Button</button>
</div>
<script>
const parent = document.getElementById('parent');
const child = document.getElementById('child');
parent.addEventListener('click', () => {
console.log('Parent clicked');
});
child.addEventListener('click', (e) => {
e.stopPropagation(); // ✅ prevents bubbling to parent
console.log('Child clicked');
});
</script>Child clicked✅ Parent does NOT log
✅ Because stopPropagation() stops the event from bubbling up
Parent clicked✅ Works normally
✅ Because no child event was triggered
stopImmediatePropagation()
Stops bubbling/capturing, just like
stopPropagation()AND stops other event listeners on the SAME element from running
button.addEventListener('click', () => {
console.log('Listener 1');
});
button.addEventListener('click', (e) => {
e.stopImmediatePropagation();
console.log('Listener 2');
});
button.addEventListener('click', () => {
console.log('Listener 3');
});
// Output : Listener 2Why?
Listener 2runs firstIt calls
stopImmediatePropagation()So:
❌ No more listeners on button
❌ Event does NOT bubble up
✅ Only
Listener 2executes
🆚 Quick difference
Method | Stops bubbling? | Stops other listeners on same element? |
|---|---|---|
| ✅ Yes | ❌ No |
| ✅ Yes | ✅ Yes |
🔍 Quick Comparison
Feature | Capturing | Bubbling |
|---|---|---|
Direction | Top → Target | Target → Top |
Default? | ❌ No | ✅ Yes |
Enable using |
|
|
Common usage | Rare | Very common (delegation) |
<div class="infographic-jspropagation">
<style>
.infographic-jspropagation {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--line: #ddd6c9;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--ok: #1a7f4e;
--ok-soft: #e3f5ec;
--warn: #b45309;
--warn-soft: #fdf0dc;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', -apple-system, sans-serif;
--mono-font: 'IBM Plex Mono', 'Courier New', monospace;
margin: 0 auto;
box-sizing: border-box;
font-family: var(--body-font);
color: var(--ink);
background: var(--paper);
}
.infographic-jspropagation[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--line: #2c313d;
--accent: #5b8def;
--accent-soft:#202942;
--ok: #5cd6a0;
--ok-soft: #16342a;
--warn: #e0a458;
--warn-soft: #3a2c14;
}
.infographic-jspropagation * { box-sizing: border-box; }
.infographic-jspropagation .ig-inner { padding: 1.75rem clamp(1.25rem, 5vw, 3rem) 2rem; }
.infographic-jspropagation .ig-title {
font-family: var(--display-font); font-size: clamp(1.4rem, 4.5vw, 1.9rem);
font-weight: 600; text-align: center; margin: 0 0 1.25rem;
}
.infographic-jspropagation .controls { display: flex; align-items: center; justify-content: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
.infographic-jspropagation .ctrl-btn {
font: inherit; font-size: 0.82rem; font-weight: 600; border: 1px solid var(--line);
background: var(--paper-alt); color: var(--ink); padding: 0.45rem 1rem;
border-radius: 999px; cursor: pointer;
}
.infographic-jspropagation .ctrl-btn:hover { border-color: var(--accent); color: var(--accent); }
.infographic-jspropagation .ctrl-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.infographic-jspropagation .toggle-row { display: flex; align-items: center; justify-content: center; gap: 0.6rem; margin-bottom: 1.5rem; }
.infographic-jspropagation .toggle-label { font-family: var(--mono-font); font-size: 0.78rem; color: var(--ink-soft); }
.infographic-jspropagation .switch {
position: relative; width: 3.2rem; height: 1.7rem; border-radius: 999px; cursor: pointer;
background: var(--line); transition: background 0.3s;
}
.infographic-jspropagation .switch.is-on { background: var(--warn); }
.infographic-jspropagation .switch .knob {
position: absolute; top: 0.17rem; left: 0.17rem; width: 1.36rem; height: 1.36rem; border-radius: 50%;
background: #fff; transition: transform 0.3s cubic-bezier(.4,1.4,.6,1); box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.infographic-jspropagation .switch.is-on .knob { transform: translateX(1.5rem); }
/* Nested DOM tree */
.infographic-jspropagation .stage {
position: relative; width: 280px; height: 280px; margin: 0 auto;
}
.infographic-jspropagation .node {
position: absolute; top: 50%; left: 50%; border-radius: 12px;
border: 2px solid var(--line); background: var(--paper-alt);
display: flex; align-items: flex-start; justify-content: center;
padding-top: 0.35rem; font-family: var(--mono-font); font-weight: 700; font-size: 0.72rem;
color: var(--ink-soft); transform: translate(-50%, -50%);
transition: border-color 0.25s ease, background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
}
.infographic-jspropagation .node.capturing { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.infographic-jspropagation .node.is-target { border-color: var(--warn); color: var(--warn); background: var(--warn-soft); box-shadow: 0 0 0 4px var(--warn-soft); }
.infographic-jspropagation .node.bubbling { border-color: var(--ok); color: var(--ok); background: var(--ok-soft); }
.infographic-jspropagation .node.has-listener::after {
content: "●"; font-size: 0.55rem; margin-left: 0.3rem; color: inherit; opacity: 0.7;
}
.infographic-jspropagation .legend { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-top: 1rem; }
.infographic-jspropagation .legend-item { display: flex; align-items: center; gap: 0.35rem; font-size: 0.72rem; color: var(--ink-soft); }
.infographic-jspropagation .swatch { width: 0.7rem; height: 0.7rem; border-radius: 3px; }
.infographic-jspropagation .swatch.cap { background: var(--accent); }
.infographic-jspropagation .swatch.tgt { background: var(--warn); }
.infographic-jspropagation .swatch.bub { background: var(--ok); }
.infographic-jspropagation .console {
margin-top: 1.5rem; background: #1e1e1e; border-radius: 8px; padding: 0.9rem 1rem;
min-height: 3rem; font-family: var(--mono-font); font-size: 0.82rem; box-shadow: 0 8px 20px rgba(0,0,0,0.22);
}
.infographic-jspropagation .console-line { opacity: 0; transform: translateY(4px); transition: opacity 0.25s ease, transform 0.25s ease; }
.infographic-jspropagation .console-line.is-in { opacity: 1; transform: translateY(0); }
.infographic-jspropagation .console-line .prompt { color: #6a9955; margin-right: 0.4rem; }
.infographic-jspropagation .console-line .txt { color: #6bcf7f; font-weight: 700; }
.infographic-jspropagation .console-line .txt.stopped { color: #e57373; }
.infographic-jspropagation .caption {
text-align: center; font-size: 0.82rem; color: var(--ink-soft); margin-top: 1rem; min-height: 1.2em;
}
.infographic-jspropagation .caption strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-jspropagation *, .infographic-jspropagation *::before, .infographic-jspropagation *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ig-inner">
<h2 class="ig-title">Event Propagation</h2>
<div class="controls">
<button class="ctrl-btn primary" id="playBtn">▶ Play</button>
<button class="ctrl-btn" id="restartBtn">⟲ Restart</button>
</div>
<div class="toggle-row">
<span class="toggle-label">child.stopPropagation()</span>
<div class="switch" id="stopSwitch"><div class="knob"></div></div>
</div>
<div class="stage" id="stage"></div>
<div class="legend">
<span class="legend-item"><span class="swatch cap"></span>capturing (down)</span>
<span class="legend-item"><span class="swatch tgt"></span>target</span>
<span class="legend-item"><span class="swatch bub"></span>bubbling (up)</span>
</div>
<div class="console" id="consoleBody"></div>
<p class="caption" id="caption">● marks elements with a click listener. Press Play to trace one click on #child.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-jspropagation')
|| document.querySelector('.infographic-jspropagation');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const LEVELS = [
{ id: 'window', label: 'window', size: 280, hasListener: false },
{ id: 'document', label: 'document', size: 236, hasListener: false },
{ id: 'html', label: 'html', size: 192, hasListener: false },
{ id: 'body', label: 'body', size: 148, hasListener: false },
{ id: 'parent', label: '#parent', size: 104, hasListener: true },
{ id: 'child', label: '#child', size: 56, hasListener: true, isTarget: true }
];
const stage = wrapper.querySelector('#stage');
const playBtn = wrapper.querySelector('#playBtn');
const restartBtn = wrapper.querySelector('#restartBtn');
const stopSwitch = wrapper.querySelector('#stopSwitch');
const consoleBody = wrapper.querySelector('#consoleBody');
const caption = wrapper.querySelector('#caption');
let stopProp = false;
let nodeEls = {};
function buildStage() {
stage.innerHTML = '';
nodeEls = {};
LEVELS.forEach((lvl, i) => {
const el = document.createElement('div');
el.className = 'node' + (lvl.hasListener ? ' has-listener' : '');
el.style.width = lvl.size + 'px';
el.style.height = lvl.size + 'px';
el.style.zIndex = i;
el.textContent = lvl.label;
stage.appendChild(el);
nodeEls[lvl.id] = el;
});
}
buildStage();
stopSwitch.addEventListener('click', () => {
stopProp = !stopProp;
stopSwitch.classList.toggle('is-on', stopProp);
restart();
});
function resetVisual() {
LEVELS.forEach(lvl => nodeEls[lvl.id].classList.remove('capturing', 'is-target', 'bubbling'));
consoleBody.innerHTML = '';
}
function addConsoleLine(text, stopped) {
const line = document.createElement('div');
line.className = 'console-line';
line.innerHTML = `<span class="prompt">></span><span class="txt${stopped ? ' stopped' : ''}">${text}</span>`;
consoleBody.appendChild(line);
requestAnimationFrame(() => line.classList.add('is-in'));
}
let steps = [];
function buildSteps() {
steps = [];
LEVELS.forEach(lvl => steps.push({ phase: 'capture', lvl }));
steps.push({ phase: 'target', lvl: LEVELS[LEVELS.length - 1] });
const bubbleOrder = LEVELS.slice().reverse();
for (let i = 0; i < bubbleOrder.length; i++) {
const lvl = bubbleOrder[i];
steps.push({ phase: 'bubble', lvl });
if (lvl.id === 'child' && stopProp) {
steps.push({ phase: 'stopped' });
break;
}
}
}
let timer = null;
let idx = -1;
function applyStep(step) {
if (step.phase === 'capture') {
nodeEls[step.lvl.id].classList.add('capturing');
caption.innerHTML = `capturing → reaching <strong>${step.lvl.label}</strong>`;
} else if (step.phase === 'target') {
nodeEls[step.lvl.id].classList.remove('capturing');
nodeEls[step.lvl.id].classList.add('is-target');
caption.innerHTML = `<strong>target phase</strong> — the click actually happened on #child`;
} else if (step.phase === 'bubble') {
nodeEls[step.lvl.id].classList.remove('is-target');
nodeEls[step.lvl.id].classList.add('bubbling');
if (step.lvl.hasListener) {
const text = step.lvl.id === 'child' ? 'Child clicked' : 'Parent clicked';
addConsoleLine(text, false);
}
caption.innerHTML = `bubbling → passing <strong>${step.lvl.label}</strong>${step.lvl.hasListener ? ' (has a listener)' : ''}`;
} else if (step.phase === 'stopped') {
addConsoleLine('e.stopPropagation() — bubbling halted here', true);
caption.innerHTML = `<strong>stopPropagation()</strong> stopped the event right after #child — it never reached #parent`;
}
}
function stop() {
clearInterval(timer);
timer = null;
playBtn.textContent = '▶ Play';
}
function advance() {
idx++;
if (idx >= steps.length) { stop(); return; }
applyStep(steps[idx]);
if (idx >= steps.length - 1) stop();
}
function play() {
if (timer) { stop(); return; }
if (idx >= steps.length - 1) restart(false);
playBtn.textContent = '⏸ Pause';
timer = setInterval(advance, 550);
advance();
}
function restart(clearCaption = true) {
stop();
idx = -1;
resetVisual();
buildSteps();
if (clearCaption) caption.textContent = '● marks elements with a click listener. Press Play to trace one click on #child.';
}
playBtn.addEventListener('click', play);
restartBtn.addEventListener('click', () => restart(true));
restart(true);
})();
</script>.demo {
font-family: system-ui, sans-serif;
padding: 16px;
}
.demo h1 {
color: #2563eb;
margin: 0 0 8px;
}
.demo p {
color: #4b5563;
margin: 0;
}document.querySelector('.demo h1')?.addEventListener('click', () => {
alert('Hello from live preview!');
});