How to Add Event Listeners in JS: Complete Guide

Master addEventListener in JavaScript. Learn every event type, handler options, event removal, once and passive listeners, and real-world patterns with examples.

JavaScriptbeginner
11 min read

Events are the backbone of interactive web pages. Every time a user clicks a button, types in an input, scrolls the page, or hovers over an element, the browser fires an event. JavaScript lets you listen for these events and run code in response using the addEventListener method. This guide covers the full addEventListener API with practical examples for every common event type.

What is an Event Listener?

An event listener is a function that waits for a specific event to happen on an element and then executes your code. Think of it as setting a trap: you tell the browser "when this happens on that element, run this function."

javascriptjavascript
const button = document.getElementById("submit-btn");
 
// When the button is clicked, run this function
button.addEventListener("click", function () {
  console.log("Button was clicked!");
});

addEventListener Syntax

javascriptjavascript
element.addEventListener(eventType, handler, options);
ParameterTypeRequiredDescription
eventTypeStringYesThe event name: "click", "keydown", "submit", etc.
handlerFunctionYesThe callback function to execute when the event fires
optionsObject or BooleanNoConfiguration: capture, once, passive, signal
javascriptjavascript
const input = document.getElementById("search");
 
// Named function handler
function handleInput(event) {
  console.log("User typed:", event.target.value);
}
 
input.addEventListener("input", handleInput);

Common Event Types

Mouse Events

javascriptjavascript
const box = document.getElementById("interactive-box");
 
// Click (press and release)
box.addEventListener("click", () => console.log("Clicked"));
 
// Double click
box.addEventListener("dblclick", () => console.log("Double clicked"));
 
// Mouse button pressed down
box.addEventListener("mousedown", () => console.log("Mouse down"));
 
// Mouse button released
box.addEventListener("mouseup", () => console.log("Mouse up"));
 
// Mouse moves over the element
box.addEventListener("mousemove", (e) => {
  console.log(`Mouse at: ${e.clientX}, ${e.clientY}`);
});
 
// Mouse enters the element
box.addEventListener("mouseenter", () => console.log("Mouse entered"));
 
// Mouse leaves the element
box.addEventListener("mouseleave", () => console.log("Mouse left"));

Keyboard Events

javascriptjavascript
const input = document.getElementById("text-input");
 
// Key pressed down
input.addEventListener("keydown", (e) => {
  console.log("Key down:", e.key);
});
 
// Key released
input.addEventListener("keyup", (e) => {
  console.log("Key up:", e.key);
});

Form Events

javascriptjavascript
const form = document.getElementById("signup-form");
const emailInput = document.getElementById("email");
 
// Form submitted
form.addEventListener("submit", (e) => {
  e.preventDefault(); // Stop the default form submission
  console.log("Form submitted");
});
 
// Input value changed (fires on every keystroke)
emailInput.addEventListener("input", (e) => {
  console.log("Current value:", e.target.value);
});
 
// Input lost focus
emailInput.addEventListener("blur", () => {
  console.log("Input lost focus");
});
 
// Input gained focus
emailInput.addEventListener("focus", () => {
  console.log("Input focused");
});
 
// Value committed (fires on blur or Enter)
emailInput.addEventListener("change", (e) => {
  console.log("Value committed:", e.target.value);
});

Window and Document Events

javascriptjavascript
// Page fully loaded (including images, stylesheets)
window.addEventListener("load", () => {
  console.log("Page fully loaded");
});
 
// DOM ready (HTML parsed, before images load)
document.addEventListener("DOMContentLoaded", () => {
  console.log("DOM is ready");
});
 
// Window resized
window.addEventListener("resize", () => {
  console.log(`Window: ${window.innerWidth}x${window.innerHeight}`);
});
 
// Page scrolled
window.addEventListener("scroll", () => {
  console.log("Scroll position:", window.scrollY);
});

The Event Object

Every event handler receives an event object with details about what happened:

javascriptjavascript
document.addEventListener("click", (event) => {
  console.log("Event type:", event.type);         // "click"
  console.log("Target element:", event.target);    // The clicked element
  console.log("Current target:", event.currentTarget); // Element with the listener
  console.log("Mouse X:", event.clientX);          // X position in viewport
  console.log("Mouse Y:", event.clientY);          // Y position in viewport
  console.log("Timestamp:", event.timeStamp);      // When the event fired
});

Key Event Object Properties

PropertyDescriptionEvent Types
event.targetThe element that triggered the eventAll
event.currentTargetThe element the listener is attached toAll
event.typeThe event name ("click", "keydown", etc.)All
event.preventDefault()Stops the default browser behaviorAll
event.stopPropagation()Stops the event from bubbling upAll
event.keyThe key that was pressed ("Enter", "a", etc.)Keyboard
event.clientX / clientYMouse position in the viewportMouse
event.pageX / pageYMouse position in the documentMouse

Listener Options

The third parameter of addEventListener accepts an options object:

javascriptjavascript
element.addEventListener("click", handler, {
  capture: false,  // Use capture phase instead of bubble phase
  once: true,      // Automatically remove after first trigger
  passive: true,   // Promise not to call preventDefault()
  signal: controller.signal  // AbortSignal for removal
});

once: Auto-Remove After First Trigger

javascriptjavascript
const button = document.getElementById("one-time-btn");
 
button.addEventListener("click", () => {
  console.log("This only fires once!");
  button.textContent = "Already clicked";
  button.disabled = true;
}, { once: true });
// The listener is automatically removed after the first click

passive: Performance Optimization

javascriptjavascript
// Passive listeners improve scroll performance
// by telling the browser you won't call preventDefault()
document.addEventListener("scroll", () => {
  // Update UI based on scroll position
  const header = document.querySelector("header");
  header.classList.toggle("scrolled", window.scrollY > 100);
}, { passive: true });
 
// Touch events benefit greatly from passive listeners
document.addEventListener("touchstart", handleTouch, { passive: true });

signal: Remove with AbortController

javascriptjavascript
const controller = new AbortController();
 
document.addEventListener("click", () => {
  console.log("Click detected");
}, { signal: controller.signal });
 
document.addEventListener("keydown", (e) => {
  console.log("Key:", e.key);
}, { signal: controller.signal });
 
// Remove ALL listeners linked to this controller at once
controller.abort();

Removing Event Listeners

To remove a listener, you must pass the exact same function reference used when adding it.

javascriptjavascript
// CORRECT: Named function reference
function handleClick() {
  console.log("Clicked");
}
 
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // Works!
 
// WRONG: Anonymous functions cannot be removed
button.addEventListener("click", function () {
  console.log("Clicked");
});
button.removeEventListener("click", function () {
  console.log("Clicked");
}); // Does NOT work: different function object!

Three Reliable Removal Patterns

javascriptjavascript
// Pattern 1: Named function
function onResize() { /* ... */ }
window.addEventListener("resize", onResize);
window.removeEventListener("resize", onResize);
 
// Pattern 2: { once: true } for one-time listeners
button.addEventListener("click", () => {
  console.log("Auto-removed after this");
}, { once: true });
 
// Pattern 3: AbortController for multiple listeners
const controller = new AbortController();
button.addEventListener("click", handleClick, { signal: controller.signal });
input.addEventListener("input", handleInput, { signal: controller.signal });
// Remove both at once:
controller.abort();

Inline Event Handlers vs addEventListener

Older code uses inline HTML event handlers. Modern code should always use addEventListener:

javascriptjavascript
// OLD WAY (avoid): Inline HTML handler
// <button onclick="handleClick()">Click</button>
 
// OLD WAY (avoid): DOM property
button.onclick = function () {
  console.log("Clicked");
};
// Problem: Only ONE handler per event per element!
 
// MODERN WAY: addEventListener
button.addEventListener("click", handleClick);
button.addEventListener("click", handleAnalytics);
// Multiple handlers on the same event!
FeatureInline / onclickaddEventListener
Multiple handlers per eventNo (overwrites previous)Yes
Capture/bubble controlNoYes
once/passive optionsNoYes
Easy removalNoYes (with named function)
Separation of concernsNo (mixes HTML and JS)Yes

Best Practices

1. Always Use Named Functions for Removable Listeners

javascriptjavascript
// Good: Easy to remove later
function handleScroll() {
  if (window.scrollY > 500) {
    showBackToTop();
    window.removeEventListener("scroll", handleScroll);
  }
}
window.addEventListener("scroll", handleScroll);

2. Debounce High-Frequency Events

javascriptjavascript
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
 
// Resize fires dozens of times per second
window.addEventListener("resize", debounce(() => {
  console.log("Resize settled:", window.innerWidth);
}, 250));

3. Use Passive Listeners for Scroll and Touch

javascriptjavascript
// Always mark scroll/touch listeners as passive if you
// don't need to call preventDefault()
window.addEventListener("scroll", handleScroll, { passive: true });
document.addEventListener("touchmove", handleTouch, { passive: true });
javascriptjavascript
function createImageGallery(containerId, images) {
  const container = document.getElementById(containerId);
  const controller = new AbortController();
  const signal = controller.signal;
 
  // Build gallery HTML
  const gallery = document.createElement("div");
  gallery.className = "gallery";
 
  const viewer = document.createElement("div");
  viewer.className = "gallery-viewer";
  const mainImage = document.createElement("img");
  mainImage.src = images[0].src;
  mainImage.alt = images[0].alt;
  viewer.appendChild(mainImage);
 
  const thumbnails = document.createElement("div");
  thumbnails.className = "gallery-thumbnails";
 
  let currentIndex = 0;
 
  images.forEach((img, index) => {
    const thumb = document.createElement("img");
    thumb.src = img.thumb;
    thumb.alt = img.alt;
    thumb.className = index === 0 ? "thumb active" : "thumb";
 
    thumb.addEventListener("click", () => {
      currentIndex = index;
      updateView();
    }, { signal });
 
    thumbnails.appendChild(thumb);
  });
 
  function updateView() {
    mainImage.src = images[currentIndex].src;
    mainImage.alt = images[currentIndex].alt;
    thumbnails.querySelectorAll(".thumb").forEach((t, i) => {
      t.classList.toggle("active", i === currentIndex);
    });
  }
 
  // Keyboard navigation
  document.addEventListener("keydown", (e) => {
    if (e.key === "ArrowRight" && currentIndex < images.length - 1) {
      currentIndex++;
      updateView();
    } else if (e.key === "ArrowLeft" && currentIndex > 0) {
      currentIndex--;
      updateView();
    }
  }, { signal });
 
  // Swipe support for mobile
  let touchStartX = 0;
 
  viewer.addEventListener("touchstart", (e) => {
    touchStartX = e.touches[0].clientX;
  }, { signal, passive: true });
 
  viewer.addEventListener("touchend", (e) => {
    const touchEndX = e.changedTouches[0].clientX;
    const diff = touchStartX - touchEndX;
 
    if (Math.abs(diff) > 50) {
      if (diff > 0 && currentIndex < images.length - 1) {
        currentIndex++;
      } else if (diff < 0 && currentIndex > 0) {
        currentIndex--;
      }
      updateView();
    }
  }, { signal });
 
  gallery.append(viewer, thumbnails);
  container.appendChild(gallery);
 
  // Return cleanup function
  return {
    destroy() {
      controller.abort(); // Remove ALL listeners at once
      container.removeChild(gallery);
    },
    goTo(index) {
      currentIndex = Math.max(0, Math.min(index, images.length - 1));
      updateView();
    }
  };
}
 
const gallery = createImageGallery("app", [
  { src: "/img/photo1.jpg", thumb: "/img/photo1-sm.jpg", alt: "Sunset" },
  { src: "/img/photo2.jpg", thumb: "/img/photo2-sm.jpg", alt: "Mountain" },
  { src: "/img/photo3.jpg", thumb: "/img/photo3-sm.jpg", alt: "Ocean" }
]);
Rune AI

Rune AI

Key Insights

  • addEventListener over onclick: addEventListener supports multiple handlers, options, and clean removal; onclick allows only one handler per element
  • Named functions for removal: Always store handlers in variables so you can pass the same reference to removeEventListener
  • AbortController for cleanup: Link multiple listeners to one controller and call abort() to remove them all at once
  • Passive for performance: Use { passive: true } on scroll, wheel, and touch events to avoid blocking the browser's rendering
  • once for one-time events: Use { once: true } instead of manually removing the listener inside the handler
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between addEventListener and onclick?

The `addEventListener` method lets you attach multiple handlers to the same event on the same element, provides options like `once`, `passive`, and `capture`, and supports clean removal with `removeEventListener`. The `onclick` property only allows one handler per element (each assignment overwrites the previous one) and offers no configuration options. Always prefer `addEventListener` in modern code.

Why does removeEventListener not work with anonymous functions?

The `removeEventListener` method compares function references to find and remove the correct handler. Two anonymous functions that look identical are still different objects in memory, so the browser cannot match them. Always store your handler in a named variable or use `{ once: true }` or `AbortController` for automatic removal.

When should I use passive event listeners?

Use `{ passive: true }` for scroll, wheel, and touch events when you do not need to call `preventDefault()`. Passive listeners let the browser optimize scrolling performance because it knows the handler will not block the scroll. Chrome, Firefox, and Safari now default `touchstart` and `touchmove` listeners to passive on `document` and `window`.

How do I listen for events on elements that do not exist yet?

Use [event delegation](/tutorials/programming-languages/javascript/javascript-event-delegation-complete-tutorial): attach the listener to a parent element that already exists, then check `event.target` inside the handler to see which child triggered the event. This works because events [bubble up](/tutorials/programming-languages/javascript/javascript-event-bubbling-explained-for-beginners) from the target element through all its ancestors.

Can I add the same event listener twice?

If you pass the exact same function reference, event type, and capture option, the browser ignores the duplicate. It will not add the same listener twice. However, two different function objects (even with identical code) are treated as separate listeners and both will run.

Conclusion

The addEventListener method is the foundation of all JavaScript interactivity. It attaches handlers to any DOM element for any event type, supports multiple handlers per event, and provides fine-grained control through options like once, passive, and signal. For removal, always use named functions or an AbortController to manage listener lifecycles cleanly. Mark scroll and touch handlers as passive for better performance, and use debouncing for high-frequency events like resize and mousemove. These patterns give you complete control over how your pages respond to user interaction.