JavaScript Event Listener
addEventListener() is the standard way to register event handlers in modern JavaScript. This chapter takes a deeper look at its options, event propagation (bubbling vs capturing), event delegation, and best practices for cleaning up listeners.
addEventListener() Syntax
The full signature is: element.addEventListener(type, listener, options). The options parameter is an object that can include capture, once, passive, and signal.
const btn = document.getElementById("myBtn");
function handleClick(e) {
console.log("Clicked!", e.type);
}
// Basic usage
btn.addEventListener("click", handleClick);
// With options object
btn.addEventListener("click", handleClick, {
capture: false, // listen in bubble phase (default)
once: false, // do not auto-remove after first call
passive: false // allow preventDefault()
});
console.log("Listener attached with options");Multiple Listeners on One Element
Unlike on-event properties, addEventListener lets you attach multiple handlers for the same event on the same element. They run in the order they were registered.
const input = document.getElementById("search");
input.addEventListener("input", () => {
console.log("Listener 1: validate input");
});
input.addEventListener("input", () => {
console.log("Listener 2: update suggestions");
});
input.addEventListener("input", () => {
console.log("Listener 3: track analytics");
});
// When the user types, all three fire in order:
// Listener 1: validate input
// Listener 2: update suggestions
// Listener 3: track analyticsEvent Options: once, passive, capture
The options object gives fine-grained control over how and when the listener fires.
| Option | Type | Description |
|---|---|---|
| capture | boolean | If true, fires during the capture phase (top-down) instead of the bubble phase (bottom-up) |
| once | boolean | If true, the listener auto-removes itself after firing once |
| passive | boolean | If true, tells the browser the handler will never call preventDefault() — improves scroll performance |
| signal | AbortSignal | An AbortSignal; when aborted, the listener is removed |
const btn = document.getElementById("myBtn");
// Fires only once, then auto-removes
btn.addEventListener("click", () => {
console.log("This runs only once!");
}, { once: true });
// Passive scroll listener — improves performance
window.addEventListener("scroll", () => {
console.log("Scrolled! Y:", window.scrollY);
}, { passive: true });
console.log("Listeners registered");Bubbling vs Capturing
When an event fires, it goes through three phases: capture (root to target), target, and bubble (target back to root). By default listeners fire in the bubble phase. Set capture: true to listen during the capture phase instead.
// <div id="outer"><button id="inner">Click</button></div>
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
outer.addEventListener("click", () => {
console.log("outer — capture phase");
}, { capture: true });
inner.addEventListener("click", () => {
console.log("inner — target/bubble phase");
});
outer.addEventListener("click", () => {
console.log("outer — bubble phase");
});
// Clicking the inner button logs:
// outer — capture phase
// inner — target/bubble phase
// outer — bubble phaseEvent Delegation
Instead of attaching a listener to every child element, attach one listener to a common parent and use event.target to determine which child was clicked. This is more efficient and works for dynamically added elements.
const list = document.getElementById("todo-list");
// One listener on the parent <ul>
list.addEventListener("click", (e) => {
// Only act if the click was on an <li>
if (e.target.tagName === "LI") {
e.target.classList.toggle("done");
console.log("Toggled:", e.target.textContent);
}
});
// Works even for items added later!
const newItem = document.createElement("li");
newItem.textContent = "New task";
list.appendChild(newItem);
console.log("New item added — delegation handles it automatically");function onResize() {
console.log("Window resized:", window.innerWidth);
}
window.addEventListener("resize", onResize);
// Later, clean up
window.removeEventListener("resize", onResize);
console.log("Resize listener removed");
// Using AbortController (modern alternative)
const controller = new AbortController();
window.addEventListener("resize", onResize, { signal: controller.signal });
// Remove all listeners attached with this signal
controller.abort();
console.log("All listeners aborted via AbortController");