Skip to content
JavaScript js events 4 min read

addEventListener

addEventListener is the standard way to make an element respond to events. Unlike inline on* attributes or DOM properties, it lets you attach multiple handlers to the same target, configure precise behavior through an options object, and detach handlers cleanly with removeEventListener. Getting this right is what keeps long-lived apps responsive and free of memory leaks.

The signature

Every DOM node, plus document and window, exposes addEventListener. It takes the event type, the handler function, and an optional third argument that tunes how the listener behaves.

target.addEventListener(type, handler, options);
ParameterTypeDescription
typestringThe event name, without the on prefix — "click", "input", "keydown".
handlerfunctionCalled with the Event object when the event fires.
optionsobject | booleanOptional. An options object, or a boolean shorthand for capture.

The handler receives the Event as its first argument, and inside it this refers to the element the listener is attached to (use a regular function, not an arrow, if you rely on this).

const btn = document.querySelector("#save");

btn.addEventListener("click", function (event) {
  console.log("clicked", event.type, this.id);
});

Output:

clicked click save

Attaching multiple handlers

The key advantage over element.onclick = fn is that listeners stack. Each call to addEventListener adds another handler; they all run, in the order they were registered. Adding the exact same function reference for the same type and capture phase twice is a no-op — the duplicate is ignored.

const btn = document.querySelector("#save");

btn.addEventListener("click", () => console.log("validate"));
btn.addEventListener("click", () => console.log("persist"));
btn.addEventListener("click", () => console.log("notify"));
// One click logs all three, in order.

The options object

The third argument unlocks behavior you cannot get any other way. Pass an object with any combination of these flags.

OptionDefaultEffect
oncefalseThe handler runs at most once, then auto-removes itself.
capturefalseFire during the capture phase (top-down) instead of bubbling (bottom-up).
passivefalsePromises you won’t call preventDefault, letting the browser optimize scrolling.
signalAn AbortSignal; aborting it removes the listener.
// Runs exactly once, then detaches automatically.
button.addEventListener("click", handleFirstClick, { once: true });

// Improves scroll performance for high-frequency events.
window.addEventListener("scroll", onScroll, { passive: true });

Passing a boolean as the third argument is the legacy form: true means { capture: true }. Prefer the explicit object — it is clearer and supports the other flags.

The interactive demo below shows once and stacked handlers working together. Click the buttons and watch the log.

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui, sans-serif; padding: 24px; }
    button { font-size: 1rem; padding: 8px 14px; margin-right: 8px; cursor: pointer; }
    #log { margin-top: 16px; font-family: monospace; white-space: pre-line; }
  </style>
</head>
<body>
  <button id="multi">Stacked handlers</button>
  <button id="once">Runs once</button>
  <div id="log"></div>

  <script>
    const log = document.querySelector("#log");
    const print = (msg) => (log.textContent += msg + "\n");

    const multi = document.querySelector("#multi");
    multi.addEventListener("click", () => print("handler A"));
    multi.addEventListener("click", () => print("handler B"));

    const once = document.querySelector("#once");
    once.addEventListener("click", () => print("fired (won't fire again)"), {
      once: true,
    });
  </script>
</body>
</html>

Removing listeners

removeEventListener(type, handler, options) detaches a previously added listener. The catch: you must pass the same function reference and a matching capture value. An inline arrow function can never be removed because you have no reference to it.

function onResize() {
  console.log("window resized");
}

window.addEventListener("resize", onResize);
// Later — must be the same named reference:
window.removeEventListener("resize", onResize);

This is why the example below stores the handler in a variable. The named reference is what makes removal possible.

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui, sans-serif; padding: 24px; }
    button { font-size: 1rem; padding: 8px 14px; margin-right: 8px; cursor: pointer; }
    #area { margin-top: 16px; padding: 24px; border: 2px dashed #888; }
  </style>
</head>
<body>
  <button id="stop">Disable tracking</button>
  <div id="area">Move your mouse here. Position: <span id="pos">—</span></div>

  <script>
    const area = document.querySelector("#area");
    const pos = document.querySelector("#pos");

    function trackMouse(event) {
      pos.textContent = `${event.offsetX}, ${event.offsetY}`;
    }

    area.addEventListener("mousemove", trackMouse);

    document.querySelector("#stop").addEventListener("click", () => {
      area.removeEventListener("mousemove", trackMouse);
      pos.textContent = "tracking off";
    });
  </script>
</body>
</html>

Cleanup with AbortController

Removing many listeners by hand is tedious. An AbortController lets you tear down every listener bound to one signal in a single call — ideal when a component unmounts or a view is destroyed.

const controller = new AbortController();
const { signal } = controller;

input.addEventListener("input", onInput, { signal });
window.addEventListener("resize", onResize, { signal });
document.addEventListener("keydown", onKey, { signal });

// One call removes all three listeners at once:
controller.abort();

Listeners on elements that stay in the DOM keep their handler functions — and everything those closures capture — alive in memory. If you create and discard elements over a session without removing their listeners, you leak. Always clean up.

Best Practices

  • Prefer addEventListener over on* properties so you can stack handlers and use options.
  • Keep a named reference for any handler you intend to remove; arrow functions passed inline cannot be detached.
  • Use { once: true } for one-shot handlers instead of manually removing them inside the callback.
  • Add { passive: true } to scroll, touchmove, and wheel listeners to keep scrolling smooth.
  • Use an AbortController signal to remove groups of listeners in one call during cleanup.
  • Match the capture flag when removing — a listener added with { capture: true } won’t be removed by a default-phase call.
  • Always remove listeners (or use a signal) when the target outlives the listener, to avoid memory leaks.
Last updated June 1, 2026
Was this helpful?