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.
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."
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
element.addEventListener(eventType, handler, options);| Parameter | Type | Required | Description |
|---|---|---|---|
eventType | String | Yes | The event name: "click", "keydown", "submit", etc. |
handler | Function | Yes | The callback function to execute when the event fires |
options | Object or Boolean | No | Configuration: capture, once, passive, signal |
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
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
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
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
// 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:
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
| Property | Description | Event Types |
|---|---|---|
event.target | The element that triggered the event | All |
event.currentTarget | The element the listener is attached to | All |
event.type | The event name ("click", "keydown", etc.) | All |
event.preventDefault() | Stops the default browser behavior | All |
event.stopPropagation() | Stops the event from bubbling up | All |
event.key | The key that was pressed ("Enter", "a", etc.) | Keyboard |
event.clientX / clientY | Mouse position in the viewport | Mouse |
event.pageX / pageY | Mouse position in the document | Mouse |
Listener Options
The third parameter of addEventListener accepts an options object:
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
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 clickpassive: Performance Optimization
// 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
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.
// 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
// 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:
// 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!| Feature | Inline / onclick | addEventListener |
|---|---|---|
| Multiple handlers per event | No (overwrites previous) | Yes |
| Capture/bubble control | No | Yes |
| once/passive options | No | Yes |
| Easy removal | No | Yes (with named function) |
| Separation of concerns | No (mixes HTML and JS) | Yes |
Best Practices
1. Always Use Named Functions for Removable Listeners
// 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
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
// 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 });Real-World Example: Interactive Image Gallery
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
Key Insights
- addEventListener over onclick:
addEventListenersupports multiple handlers, options, and clean removal;onclickallows 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
Frequently Asked Questions
What is the difference between addEventListener and onclick?
Why does removeEventListener not work with anonymous functions?
When should I use passive event listeners?
How do I listen for events on elements that do not exist yet?
Can I add the same event listener twice?
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.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.