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.

6 min read

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.

javascriptjavascript
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.
  • button is 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:

javascriptjavascript
element.addEventListener(type, listener);
element.addEventListener(type, listener, options);
element.addEventListener(type, listener, useCapture);
ParameterRequiredDescription
typeYesThe event name as a string, like "click", "keydown", or "submit"
listenerYesA function that runs when the event fires
optionsNoAn object with optional flags: capture, once, passive, signal
useCaptureNoA 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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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.

javascriptjavascript
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
});
PropertyWhat it tells you
event.typeThe event name ("click", "keydown", etc.)
event.targetThe element that triggered the event (useful in event delegation)
event.currentTargetThe 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 }:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
// 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); // Works

Forgetting 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

SituationRecommendation
One-time action (modal close, welcome banner){ once: true }
Scroll or touch handler{ passive: true }
Need to remove laterNamed function + removeEventListener
Multiple listeners to clean up{ signal } with AbortController
Intercept before children{ capture: true }
Simple permanent handlerArrow 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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between addEventListener and onclick?

addEventListener lets you attach multiple handlers to the same event. It also gives you control over the capture and bubble phases, and you can remove listeners later. onclick is a property that can only hold one handler at a time and gets overwritten if you assign a new one.

Can I use addEventListener on elements that do not exist yet?

No. The element must exist in the DOM when you call addEventListener. For dynamically created elements, attach the listener right after creating the element, or use event delegation by adding the listener to a parent that already exists.

How do I remove an event listener?

Use removeEventListener with the same event type and function reference. Named functions work best because you need a reference to the exact same function. Anonymous functions and arrow functions created inline cannot be removed.

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.