Event Listeners
addEventListener is the workhorse of every interactive page on the web. The basic call is two arguments — an event name and a function — but the real depth is in the third argument: the options object. Capture phase, once-only listeners, passive scroll, and the modern AbortController cleanup story all live there. This page covers everything you actually need to manage listeners safely.
The basic signature
target.addEventListener(type, listener, options); target.removeEventListener(type, listener, options);
type is the event name as a string. listener is the function to call. options is either a boolean (legacy useCapture flag) or an object — almost always an object in modern code.
removeEventListener needs the same reference
This is the most common bug newcomers hit. removeEventListener only removes a listener if you pass exactly the same function reference that was registered.
does NOT remove anything
btn.addEventListener("click", () => console.log("hi"));
btn.removeEventListener("click", () => console.log("hi"));
// Two different anonymous functions — the second call is a no-op.correct
function onClick() {
console.log("hi");
}
btn.addEventListener("click", onClick);
// ... later ...
btn.removeEventListener("click", onClick);The options object
Three fields cover almost every real use case.
capture — listen during the capture phase instead of the bubble phase. We unpack phases on the bubbling page.
once — automatically remove the listener after it fires once. Perfect for "fire-and-forget" handlers.
passive — promise the browser you will not call
preventDefault. Required for high-performance scrolling on mobile.
btn.addEventListener("click", onClick, {
capture: false,
once: true,
passive: true,
});once: true
run only the first time
const banner = document.querySelector(".cookie-banner");
banner.addEventListener("click", () => banner.remove(), { once: true });
// First click hides the banner; the listener is then gone automatically.This saves you from writing the boilerplate of removing the listener inside itself, and it removes the need to keep a named reference just for the cleanup.
passive: true
Touch and wheel events on mobile have a special problem. The browser cannot start scrolling until the listener has run, because the listener might call preventDefault. If many listeners are attached, scrolling stutters. Declaring a listener as passive: true tells the browser "I will never preventDefault this event" — the browser can scroll in parallel and the page feels instant.
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("touchmove", onTouchMove, { passive: true });capture: true
Most listeners run during the bubble phase. Setting capture: true puts the listener on the capture phase instead — it fires on the way down from window to the target rather than on the way back up. Useful for global instrumentation, focus trapping, and a few edge cases. See the event-bubbling page for the full phase walkthrough.
AbortController for cleanup
Tracking individual listener references for removal is painful when a component has many. Modern DOM APIs accept an AbortSignal that removes the listener when the signal is aborted. One abort() call tears down everything.
component-style cleanup
function mountWidget(root) {
const controller = new AbortController();
const { signal } = controller;
root.addEventListener("click", onClick, { signal });
window.addEventListener("resize", onResize, { signal });
document.addEventListener("keydown", onKey, { signal });
return function unmount() {
controller.abort(); // removes all three listeners in one call
};
}This pattern is now the cleanest way to manage lifetimes — no named references, no per-listener removal, no leaks if you forget one.
this inside handlers
In a regular function passed to addEventListener, this is the element (the same as event.currentTarget). In an arrow function, this is whatever this was in the enclosing scope.
btn.addEventListener("click", function () {
this; // === btn
});
btn.addEventListener("click", () => {
this; // === enclosing scope's this, NOT btn
});Listener-friendly checklist
Always pair
addEventListenerwith a removal path — same reference, or anAbortSignal.Use
once: truefor one-shot handlers instead of removing yourself.Mark scroll, wheel and touch handlers as
passive: trueunless you really needpreventDefault.Inline arrow handlers in render-on-every-update code are leaks waiting to happen — store the reference.
When you must handle the same event in many places, prefer event delegation (covered separately) over many listeners.