Events: Capture, Bubble & Event Object Deep Dive
Click kiya — event neeche utarta hai (Capture), phir upar udta hai (Bubble). target vs currentTarget samjho, stopPropagation se mat khelo.
Every DOM event travels through 3 phases before it's done: Capture, Target, and Bubble. Understanding this flow is the key to mastering event handling.
Capture Phase: The event starts at the window and travels DOWN through ancestors until it reaches the target element. Think of it as a waterfall cascading down the DOM tree.
Target Phase: The event reaches the actual element that was clicked or interacted with. Here, both capture and bubble listeners on the target fire in registration order.
Bubble Phase: The event travels UP from the target through all ancestors back to window. Most event handlers are registered for this phase.
// Visualize the 3 phases
// DOM: <div id="grandparent"> → <div id="parent"> → <button id="child">
document.getElementById('grandparent').addEventListener('click',
(e) => console.log('1. Grandparent CAPTURE'),
{ capture: true }
);
document.getElementById('parent').addEventListener('click',
(e) => console.log('2. Parent CAPTURE'),
{ capture: true }
);
document.getElementById('child').addEventListener('click',
(e) => console.log('3. Child TARGET (capture listener)')
);
document.getElementById('child').addEventListener('click',
(e) => console.log('4. Child TARGET (bubble listener)')
);
document.getElementById('parent').addEventListener('click',
(e) => console.log('5. Parent BUBBLE')
);
document.getElementById('grandparent').addEventListener('click',
(e) => console.log('6. Grandparent BUBBLE')
);
// Click the button → Output order:
// 1. Grandparent CAPTURE → 2. Parent CAPTURE → 3. Child TARGET →
// 4. Child TARGET → 5. Parent BUBBLE → 6. Grandparent BUBBLE
Most events bubble, but some don't: focus, blur, mouseenter, mouseleave. These stop at the target element.
Use addEventListener(event, handler, { capture: true }) to listen during capture phase. Default is bubble phase.
The addEventListener method and the Event Object give you full control over event handling. Let's break down every property.
addEventListener syntax: element.addEventListener(event, handler, options)
Options object:
• capture: true — Listen in capture phase instead of bubble
• once: true — Auto-remove handler after first fire (great for one-time actions)
• passive: true — Promise you won't call preventDefault (improves scroll performance)
• signal: abortController.signal — Cancel listener via AbortController
// The Event Object
document.getElementById('parent').addEventListener('click', (e) => {
console.log(e.type); // "click"
console.log(e.target); // The ACTUAL element clicked (e.g., button)
console.log(e.currentTarget); // The element THIS handler is on (parent)
console.log(e.bubbles); // true (click bubbles)
console.log(e.cancelable); // true (can preventDefault)
console.log(e.eventPhase); // 1=capture, 2=target, 3=bubble
console.log(e.isTrusted); // true (real click, not dispatchEvent)
console.log(e.timeStamp); // Milliseconds since page load
});
// addEventListener options
button.addEventListener('click', handler, {
capture: false, // Listen in bubble phase (default)
once: true, // Auto-remove after first click!
passive: true, // Promises not to call preventDefault (scroll perf)
signal: abortController.signal // Cancel via AbortController
});
// Remove with AbortController
const controller = new AbortController();
button.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // Listener removed! No removeEventListener needed!
e.target is the element that triggered the event — it NEVER changes during propagation. e.currentTarget is the element whose handler is currently running — it changes at each level of the bubble phase. Use e.currentTarget when you need to know "where is THIS handler attached."isTrusted property: true means a real user action triggered the event. false means it was dispatched via JavaScript (element.dispatchEvent()).
These three methods control event behavior — know exactly what each one does.
stopPropagation(): Stops the event from moving to the next element in the capture/bubble chain. Other handlers on the SAME element still fire!
stopImmediatePropagation(): Stops the event AND prevents other handlers on the SAME element from firing. More aggressive than stopPropagation.
preventDefault(): Prevents the browser's default action — link navigation, form submission, checkbox toggle. Only works if event.cancelable is true.
// stopPropagation vs stopImmediatePropagation
button.addEventListener('click', (e) => {
console.log('Handler 1');
e.stopPropagation(); // Stops bubble to parent
// Handler 2 and 3 on THIS element STILL fire!
});
button.addEventListener('click', (e) => {
console.log('Handler 2'); // Still runs!
});
button.addEventListener('click', (e) => {
console.log('Handler 3'); // Still runs!
});
// Parent handler does NOT run (propagation stopped)
// stopImmediatePropagation — kills everything
button.addEventListener('click', (e) => {
console.log('Handler 1');
e.stopImmediatePropagation(); // NO other handlers fire!
});
button.addEventListener('click', (e) => {
console.log('Handler 2'); // NEVER runs!
});
// preventDefault — stop browser action, not propagation
link.addEventListener('click', (e) => {
e.preventDefault(); // Don't navigate!
console.log('Link clicked but no navigation');
// Event STILL bubbles to parent! Only default action prevented.
});
preventDefault freely. Use stopPropagation sparingly — it prevents parent elements from ever seeing the event, which breaks event delegation. Use stopImmediatePropagation almost never — it's a code smell.Different event types have different behaviors. Know which events bubble and which don't.
Mouse Events: click, dblclick, mousedown, mouseup, mousemove. mouseover/mouseout BUBBLE. mouseenter/mouseleave do NOT bubble.
Keyboard Events: keydown (repeats when held!), keyup. event.key = character ("a", "Enter"). event.code = physical key ("KeyA", "Enter").
Focus Events: focus/blur do NOT bubble. focusin/focusout DO bubble. Use focusin/focusout for event delegation.
Input Events: input fires on ANY value change (typing, paste, cut). change fires on blur for text inputs, on selection for radio/checkbox.
// Mouse: over vs enter — BUBBLING difference
parent.addEventListener('mouseenter', () => console.log('enter')); // NO bubble
parent.addEventListener('mouseover', () => console.log('over')); // BUBBLES
// Moving from parent to child:
// mouseenter: fires ONCE on parent enter. No child event.
// mouseover: fires on parent, then on child, then on parent (leaving child)
// Result: mouseenter = 1 event. mouseover = 3+ events!
// Keyboard: key vs code
document.addEventListener('keydown', (e) => {
console.log(e.key); // "a" or "A" (depends on Shift/CapsLock)
console.log(e.code); // "KeyA" (always same physical key)
// Use e.key for text input logic
// Use e.code for game controls / shortcut keys
if (e.key === 'Enter') handleSubmit();
if (e.ctrlKey && e.key === 's') { e.preventDefault(); save(); }
});
// Focus: focus/blur DON'T bubble — use focusin/focusout
form.addEventListener('focusin', (e) => {
e.target.classList.add('focused'); // Works! focusin bubbles.
});
form.addEventListener('focusout', (e) => {
e.target.classList.remove('focused');
});
// Input vs Change
textInput.addEventListener('input', (e) => {
console.log(e.target.value); // Fires on EVERY keystroke
});
textInput.addEventListener('change', (e) => {
console.log(e.target.value); // Fires when user LEAVES the field
});
Touch Events: touchstart, touchmove, touchend. event.touches = all current touches, event.changedTouches = what changed in this event.
Custom Events let you create your own event types with custom data. removeEventListener has a tricky gotcha that catches everyone.
Creating Custom Events: Use new CustomEvent(type, options). The detail property carries your custom data.
Dispatching: element.dispatchEvent(event) fires the event synchronously — all handlers run before dispatchEvent returns!
// Custom Events
const event = new CustomEvent('itemAdded', {
detail: { id: 123, name: 'Biryani' },
bubbles: true,
cancelable: true
});
list.addEventListener('itemAdded', (e) => {
console.log('Item added:', e.detail.name); // "Biryani"
});
list.dispatchEvent(event); // Synchronous! Handlers run immediately.
// isTrusted = false for dispatched events
The #1 removeEventListener Bug: You must pass the EXACT SAME function reference to removeEventListener that you passed to addEventListener. Arrow functions create NEW objects each time!
// ❌ The #1 removeEventListener Bug
element.addEventListener('click', () => console.log('hi'));
element.removeEventListener('click', () => console.log('hi'));
// TWO DIFFERENT function objects! Removal FAILS!
// () => {} creates a new arrow function every time.
// ✅ Fix: Store handler reference
const handler = () => console.log('hi');
element.addEventListener('click', handler);
element.removeEventListener('click', handler); // Same reference! Works!
// ✅ Or use AbortController (modern approach)
const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // Listener removed! Clean and simple.
isTrusted = false, which you can check to distinguish programmatic from real events.Lo kar liya — Key Points:
- ✅ Every DOM event travels in 3 phases: Capture (top-down) → Target → Bubble (bottom-up)
- ✅ target is the element that triggered the event (never changes); currentTarget is the element whose handler is running (changes during bubble)
- ✅ stopPropagation stops the event from reaching other elements; stopImmediatePropagation also prevents other handlers on the same element
- ✅ preventDefault stops the browser's default action (navigation, form submit) but does NOT stop propagation
- ✅ mouseover/mouseout bubble; mouseenter/mouseleave do NOT bubble — use the bubbling versions for delegation
- ✅ focus/blur don't bubble; use focusin/focusout instead for event delegation on forms
- ✅ CustomEvent with detail carries custom data; dispatchEvent fires synchronously with isTrusted = false
- ✅ removeEventListener requires the exact same function reference — arrow functions can't be removed unless stored in a variable
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login