DOM events flow through three phases — capture, target, and bubble — and event delegation leverages bubbling to handle events on dynamic child elements with a single parent listener.
Events flow through three phases: capture (down from window), target (the element itself), and bubble (back up to window). Most events bubble by default.
Attach a single listener to a parent element and use event.target to identify the child that triggered it — works for dynamically added elements and reduces memory usage.
Supports capture (phase selection), once (auto-remove), passive (performance optimization for scroll/touch), and signal (AbortController-based removal).
stopPropagation() prevents bubbling to ancestors; stopImmediatePropagation() also blocks other handlers on the same element. preventDefault() cancels the default action but doesn't stop propagation.
Create with new CustomEvent('name', { detail, bubbles }) and dispatch with element.dispatchEvent() — supports the same propagation model as native events.
The DOM event system is built on a three-phase propagation model. Understanding how events travel through the DOM tree is essential for writing efficient event handling code and is a staple of JavaScript interviews.
When an event fires, it travels through the DOM in three phases:
window through the DOM tree toward the target element. Handlers registered with { capture: true } fire during this phase.window. This is the default phase — handlers registered without capture: true fire here.Most events bubble, but some do not — focus, blur, scroll, and load do not bubble. Use focusin/focusout as bubbling alternatives to focus/blur.
Instead of attaching listeners to every child element, attach one listener to a common parent and use event.target to identify which child triggered the event. This pattern has three advantages: fewer listeners means lower memory usage, it works automatically for elements added dynamically after the listener is attached, and it simplifies setup/teardown code. Use event.currentTarget to reference the element the listener is attached to (the parent), and event.target for the element that was actually clicked.
The third argument to addEventListener accepts an options object:
capture (boolean): Listen during capture phase instead of bubble.once (boolean): Automatically remove the listener after it fires once.passive (boolean): Promises the handler will not call preventDefault(). Critical for touchstart and wheel events — browsers can optimize scrolling performance when they know preventDefault() won't be called.signal (AbortSignal): Remove the listener by calling controller.abort() — the modern alternative to keeping function references for removeEventListener.event.stopPropagation() prevents the event from continuing to the next phase (stops bubbling up or capturing down), but other handlers on the same element still fire. event.stopImmediatePropagation() stops propagation and also prevents any remaining handlers on the same element from executing.
event.preventDefault() cancels the browser's default action (form submission, link navigation, checkbox toggle) but does not stop propagation — the event still bubbles. Only works on cancelable events (check event.cancelable). Setting { passive: true } makes preventDefault() a no-op.
Create custom events with new CustomEvent('eventName', { detail: { key: 'value' } }) and dispatch them with element.dispatchEvent(event). Listeners access the payload via event.detail. Custom events support the same bubbling behavior as native events when bubbles: true is set.
Anonymous functions passed to addEventListener cannot be removed with removeEventListener because there is no reference to match. Use named functions, store references, or use AbortController with the signal option. When using object methods as event handlers, this refers to the element, not the object — use arrow functions or .bind() to preserve context.
Fun Fact
The addEventListener method wasn't available in Internet Explorer until IE9 (2011). Before that, IE used its own proprietary attachEvent method, which didn't support the capture phase and bound 'this' to window instead of the element. This incompatibility spawned an entire generation of libraries — jQuery's primary selling point was normalizing event handling across browsers.