How to Add Event Listeners in JS: Complete Guide
Learn how to use addEventListener to make your web pages respond to clicks, key presses, form submissions, and more. A practical guide with runnable examples.
addEventListener is the method you use to tell the browser "when this element experiences this kind of event, run this function." Every interactive website feature, from button clicks to form submissions, keyboard shortcuts, and scroll effects, starts with it.
Here is the smallest useful example: a button that logs a message when clicked.
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button was clicked!");
});"click"is the event type: what you are listening for.() => { ... }is the callback: what runs when the event fires.buttonis the target: the element that receives the event.
When you click the button, the console prints Button was clicked!. That is the core pattern. Everything else is built on this foundation.
The addEventListener Syntax
The full method signature supports three forms:
element.addEventListener(type, listener);
element.addEventListener(type, listener, options);
element.addEventListener(type, listener, useCapture);| Parameter | Required | Description |
|---|---|---|
type | Yes | The event name as a string, like "click", "keydown", or "submit" |
listener | Yes | A function that runs when the event fires |
options | No | An object with optional flags: capture, once, passive, signal |
useCapture | No | A boolean (older syntax). true triggers the listener during the capture phase instead of the bubble phase |
The options object replaced the older boolean useCapture parameter, but browsers still support both forms for backward compatibility. Stick with the options object for new code since it supports more than just capture.
Choosing a Listener: Named Functions vs Arrow Functions
You can pass a named function or an anonymous function as the listener. The choice matters for two reasons: removing the listener later and controlling the this keyword.
A named function keeps a reference you can pass to removeEventListener later, and this inside it refers to the element the listener is attached to:
function handleClick(event) {
console.log(this.tagName); // logs "BUTTON"
}
button.addEventListener("click", handleClick);
// Later, you can remove it:
button.removeEventListener("click", handleClick);An arrow function is shorter to write, but it has two tradeoffs worth knowing before you reach for it. Its this comes from the surrounding scope instead of the element, and because it has no stable reference, you cannot pass it to removeEventListener:
button.addEventListener("click", (event) => {
console.log(this.tagName); // undefined (or window in non-strict mode)
});
// There is no way to remove this listener because there is no function reference.An anonymous function expression written with the function keyword instead of an arrow keeps the normal this binding, so it behaves like a named function inside the handler. It still cannot be removed, though, because it has no name to reference later:
button.addEventListener("click", function (event) {
console.log(this.tagName); // "BUTTON"
});
// Cannot remove because there is no reference to this exact function.Use named functions when you need to remove the listener later, such as for one-time setup, cleanup in single-page apps, or avoiding duplicate handlers. Use arrow functions for quick, permanent handlers where this behavior does not matter.
For practical examples of how listener options like preventDefault work in specific event types, see /javascript/handling-click-events-in-javascript-full-guide.
The Event Object
Every listener receives an event object as its first argument. This object contains information about what happened and methods to control the event.
button.addEventListener("click", (event) => {
console.log(event.type); // "click"
console.log(event.target); // The element that was clicked
console.log(event.currentTarget); // The element the listener is attached to
console.log(event.clientX); // Mouse X position
console.log(event.clientY); // Mouse Y position
});| Property | What it tells you |
|---|---|
event.type | The event name ("click", "keydown", etc.) |
event.target | The element that triggered the event (useful in event delegation) |
event.currentTarget | The element the listener is attached to |
event.preventDefault() | Stops the browser's default behavior for this event |
event.stopPropagation() | Prevents the event from bubbling up to parent elements |
You will use event.target and event.currentTarget frequently when you learn about event delegation.
The Options Object: once, passive, capture, signal
The options object gives you fine-grained control without the old useCapture boolean.
once: fire and forget
When you only need a handler to run one time, use { once: true }:
button.addEventListener("click", () => {
console.log("This fires only once.");
}, { once: true });After the first click, the listener removes itself automatically. This is cleaner than calling removeEventListener manually and avoids the risk of forgetting.
passive: better scroll performance
For scroll and touch events, the browser cannot start scrolling until it knows your listener will not call preventDefault(). Setting { passive: true } tells the browser upfront that you will not cancel the scroll:
document.addEventListener("scroll", () => {
console.log("User is scrolling.");
}, { passive: true });Without passive: true, scroll listeners can cause noticeable lag on mobile devices. In browsers other than Safari, wheel, touchstart, and touchmove listeners already default to passive: true when attached to Window, Document, or Document.body, but it is still good practice to declare it explicitly since the default does not apply to other elements or to Safari.
capture: listen during the capture phase
Events travel through the DOM in two phases: capture (down from the root to the target) and bubble (up from the target to the root). By default, listeners fire during the bubble phase. Set { capture: true } to fire during the capture phase instead:
parent.addEventListener("click", () => {
console.log("Capture phase: parent fires first.");
}, { capture: true });Capture is useful for intercepting events before child elements get them. Most of the time you do not need it, but it is the right tool when you need early interception.
signal: abort with an AbortController
The signal option lets you remove listeners by calling abort() on a controller, which is cleaner than tracking individual function references:
const controller = new AbortController();
button.addEventListener("click", () => {
console.log("Click handled.");
}, { signal: controller.signal });
// Remove all listeners tied to this signal:
controller.abort();This is especially useful when you have multiple listeners that need to be cleaned up at once, like when a modal closes or a component unmounts.
Adding Listeners to Multiple Elements
A common beginner task is attaching the same behavior to many elements. Use querySelectorAll with forEach:
const buttons = document.querySelectorAll(".action-btn");
buttons.forEach((button) => {
button.addEventListener("click", () => {
console.log(`Clicked: ${button.textContent}`);
});
});For a large number of elements, consider /javascript/javascript-event-delegation-complete-tutorial instead. Attaching a single listener to a parent is more efficient than attaching dozens or hundreds of individual listeners.
Common Mistakes
Listening for an event before the element exists. Make sure your script runs after the element is in the DOM. Either place your <script> tag at the end of <body>, use the defer attribute, or wrap your code in a DOMContentLoaded listener:
document.addEventListener("DOMContentLoaded", () => {
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Safe: DOM is ready.");
});
});Using a removed listener's reference incorrectly. removeEventListener needs the exact same function reference, not a copy. Passing a new arrow function that happens to look identical does not match the original listener, so the browser leaves it attached and nothing appears to happen:
// Wrong: this is a different function each time
button.addEventListener("click", (e) => doSomething(e));
button.removeEventListener("click", (e) => doSomething(e)); // Does nothing
// Right: same reference
function handleClick(e) { doSomething(e); }
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // WorksForgetting to remove listeners in long-lived apps. In single-page applications, listeners attached to removed DOM elements can prevent garbage collection. Clean them up when components unmount, or use { once: true } or { signal } when appropriate.
When to Use Each Option
| Situation | Recommendation |
|---|---|
| One-time action (modal close, welcome banner) | { once: true } |
| Scroll or touch handler | { passive: true } |
| Need to remove later | Named function + removeEventListener |
| Multiple listeners to clean up | { signal } with AbortController |
| Intercept before children | { capture: true } |
| Simple permanent handler | Arrow function |
Start with the simplest form, addEventListener(type, callback), and add options only when you have a specific reason. A listener that does not need to be removed, does not fire during scroll, and does not need early interception is perfectly fine without any options.
Rune AI
Key Insights
- Use addEventListener(type, callback) to listen for user interactions on DOM elements.
- Named functions let you remove listeners later with removeEventListener.
- The once option fires the listener only once and removes it automatically.
- The passive option improves scroll performance when you do not need preventDefault.
- Remove listeners when they are no longer needed to avoid memory leaks.
Frequently Asked Questions
What is the difference between addEventListener and onclick?
Can I use addEventListener on elements that do not exist yet?
How do I remove an event listener?
Conclusion
addEventListener is the standard way to make your page interactive. It gives you fine control over event handling, lets you attach multiple handlers to the same event, and supports options like once, passive, and signal for better performance and cleaner code.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.