DOM Events & Event Delegation

Med

DOM events are signals fired by the browser when something happens. Events propagate through the DOM tree in three phases: capture (top-down), target, and bubble (bottom-up). Event delegation exploits bubbling to handle events efficiently by placing a single listener on a parent element.

Interactive Visualization

DOM Events

document>
body>
div>
button
Phase: Capture
Event travels DOWN from document to target

Key Points

  • addEventListener(type, handler, options) attaches event listeners to DOM nodes
  • Events propagate in 3 phases: capture (down), target, bubble (up)
  • event.target is the element that fired; event.currentTarget is the listener element
  • Event delegation: one parent listener handles events for all children via event.target
  • stopPropagation() stops bubbling; preventDefault() cancels the default browser action
  • Options: { once: true } auto-removes, { passive: true } improves scroll performance

Code Examples

addEventListener and Event Object

const button = document.querySelector('#myBtn');

button.addEventListener('click', function(event) {
  console.log('Target:', event.target);
  console.log('CurrentTarget:', event.currentTarget);
  console.log('Type:', event.type);
});

// With options
button.addEventListener('click', handler, {
  once: true,     // auto-removes after first call
  passive: false, // allows preventDefault()
});

addEventListener is the standard way to attach events. The event object contains interaction metadata.

Bubbling vs Capturing

// <div id="outer"><div id="inner"><button id="btn">Click</button></div></div>

outer.addEventListener('click', () => console.log('outer capture'), true);
inner.addEventListener('click', () => console.log('inner capture'), true);
btn.addEventListener('click', () => console.log('btn target'));
inner.addEventListener('click', () => console.log('inner bubble'));
outer.addEventListener('click', () => console.log('outer bubble'));

// Click btn → outer capture, inner capture, btn target, inner bubble, outer bubble

Events travel down during capture, hit the target, then bubble back up.

Event Delegation Pattern

const list = document.getElementById('todo-list');

// One listener handles ALL children
list.addEventListener('click', function(event) {
  const target = event.target;

  if (target.matches('.delete-btn')) {
    target.closest('li').remove();
    return;
  }

  if (target.matches('input[type="checkbox"]')) {
    target.closest('li').classList.toggle('completed');
  }
});

// Works even for dynamically added items!

Event delegation places one listener on a parent. Use matches() and closest() to identify the child.

preventDefault and Custom Events

// Prevent default browser behavior
form.addEventListener('submit', (e) => {
  e.preventDefault();
  console.log('Form submitted via JS');
});

// stopPropagation vs stopImmediatePropagation
btn.addEventListener('click', (e) => {
  e.stopPropagation(); // stops bubbling
});

// Custom Events
const event = new CustomEvent('userLogin', {
  bubbles: true,
  detail: { username: 'alice', role: 'admin' },
});
document.querySelector('#app').dispatchEvent(event);

preventDefault stops the browser action. stopPropagation stops bubbling. CustomEvent enables app-level events.

Common Mistakes

  • Using anonymous functions then trying to removeEventListener
  • Calling stopPropagation when you only need preventDefault, breaking delegation
  • Attaching individual listeners to hundreds of children instead of delegating
  • passive: true listeners cannot call preventDefault

Interview Tips

  • Draw the 3-phase propagation: capture down, target, bubble up
  • Event delegation with dynamic list — one listener handles all items
  • event.target (origin) vs event.currentTarget (listener)
  • Passive listeners for scroll/touch performance on mobile

Related Concepts