JavaScript addEventListener and removeEventListener
Learn to react to clicks, typing and keys with addEventListener, remove listeners with removeEventListener, and use options like once, passive and signal.
What you will learn
A web page that never reacts to you is just a poster. Real pages react: a menu opens when you click it, a counter updates when you type, a form warns you about a mistake, a game moves when you press an arrow key.
All of these reactions are built with events and event listeners. In this lesson you will learn what an event is, how to listen to it with addEventListener, how to read the event object, how to stop listening with removeEventListener, and how to use the useful options such as once, passive and signal.
What is an event?
An event is something that happens on the page. The browser creates one every time:
- the user clicks a button
- the user types in a text box
- the user presses a key
- the user moves the mouse over an element
- a form is submitted
- the page finishes loading
An event listener (also called an event handler) is a function that you give to the browser and say: “When this event happens on this element, please run this function.”
Think of a doorbell. The doorbell button is the element. Someone pressing it is the event. Your reaction, walking to the door, is the listener. You do not stand at the door all day. You do other things, and the bell tells you when to act.
addEventListener: the main tool
This is the recipe. It always has the same three parts:
element.addEventListener("event-name", function-to-run);
element: the thing you are listening to (found withgetElementById,querySelector, and so on)."event-name": a string such as"click". There is noonin front of it.function-to-run: the code to run when the event happens. It is called the callback.
Here is a like button. The page has a button and a counter:
<button id="like-btn">👍 Like</button>
<span id="like-count">0</span> likes
const likeBtn = document.getElementById("like-btn");
const likeCount = document.getElementById("like-count");
let likes = 0;
likeBtn.addEventListener("click", function () {
likes++;
likeCount.textContent = likes;
});
Click Try it, then press the button in the preview. The number goes up on every click. Notice what did not happen: the code inside the function did not run when the page loaded. It waits, and runs only when a click happens.
Tip: An arrow function works too. These two lines do exactly the same thing:
btn.addEventListener("click", function () { ... })andbtn.addEventListener("click", () => { ... }). The only difference is howthisbehaves, which you will see later in this lesson.
Using a named function
You can write the function separately and pass its name. This keeps your code tidy and, as you will soon see, it lets you remove the listener later.
const likeBtn = document.getElementById("like-btn");
function handleLike() {
console.log("Liked!");
}
likeBtn.addEventListener("click", handleLike);
Look closely: it is handleLike, not handleLike(). We give the browser the function itself. The browser calls it later. If you write the brackets, the function runs immediately, and its result (usually undefined) is given to the browser instead. This is the number one beginner mistake, and it is listed again in the mistakes section.
You can add many listeners to one element
Each call to addEventListener adds one more listener. They do not replace each other. They run in the order you added them:
const likeBtn = document.getElementById("like-btn");
likeBtn.addEventListener("click", function () {
console.log("First listener");
});
likeBtn.addEventListener("click", function () {
console.log("Second listener");
});
// One click prints:
// First listener
// Second listener
This is a big advantage in real projects. One part of your code can count clicks while another part sends a report, and they do not need to know about each other.
Common events
There are hundreds of events, but a small group covers most daily work.
| Event | Fires when | Typical use |
|---|---|---|
click |
An element is clicked or tapped | Buttons, menus, cards |
dblclick |
An element is double clicked | Edit mode |
input |
The value of an input changes (on every keystroke) | Live search, character counter |
change |
A value is finished changing (select, checkbox, or input losing focus) | Dropdowns, checkboxes |
submit |
A form is submitted | Form validation |
keydown |
A key is pressed down | Shortcuts, games |
keyup |
A key is released | Search as you type |
mouseover / mouseout |
The mouse enters / leaves an element or its children | Hover effects |
mouseenter / mouseleave |
The mouse enters / leaves an element | Hover effects (simpler) |
focus / blur |
An element gets / loses keyboard focus | Highlight a field |
scroll |
An element or the page is scrolled | Back-to-top button |
resize |
The window size changes (listen on window) |
Responsive tweaks |
DOMContentLoaded |
The HTML is fully read (listen on document) |
Start your code safely |
The event object
When the browser calls your function, it gives it one argument: the event object. It is full of details about what just happened. By tradition it is named event, e or evt.
const search = document.getElementById("search");
search.addEventListener("input", function (event) {
console.log(event.type); // "input"
console.log(event.target.value); // whatever is in the box right now
});
The properties you will use most:
| Property | What it gives you |
|---|---|
event.type |
The name of the event, like "click" |
event.target |
The element where the event started (the exact thing the user clicked) |
event.currentTarget |
The element your listener is attached to |
event.key |
For keyboard events: the key, like "Enter", "a", "ArrowUp" |
event.clientX, event.clientY |
For mouse events: where the pointer is in the window |
event.ctrlKey, event.shiftKey, event.altKey |
true if that modifier key was held down |
You will see the difference between target and currentTarget in the next lessons on event propagation and event delegation.
Real use case: a live character counter
Twitter-style counters show how many characters you have left. The input event fires on every keystroke, so it is perfect.
<textarea id="bio" maxlength="100" placeholder="Write your bio..."></textarea>
<p><span id="chars">0</span> / 100 characters</p>
const bio = document.getElementById("bio");
const chars = document.getElementById("chars");
bio.addEventListener("input", function (event) {
chars.textContent = event.target.value.length;
});
Real use case: live search feedback
const search = document.getElementById("search");
const result = document.getElementById("result");
search.addEventListener("input", function (event) {
const text = event.target.value.trim();
if (text === "") {
result.textContent = "Type something...";
} else {
result.textContent = 'Searching for "' + text + '"...';
}
});
Real use case: a dark mode switch
const themeBtn = document.getElementById("theme-btn");
themeBtn.addEventListener("click", function () {
document.body.classList.toggle("dark");
});
classList.toggle("dark") adds the class if it is missing and removes it if it is there. One line, and your whole page switches theme (as long as your CSS has a body.dark rule).
Real use case: a keyboard shortcut
Many sites focus the search box when you press /. Keyboard events are usually listened to on the whole document:
const search = document.getElementById("search");
document.addEventListener("keydown", function (event) {
if (event.key === "/") {
event.preventDefault(); // do not type the "/" into the box
search.focus();
}
});
preventDefault() stops the browser’s own action for this key. You will study it in its own lesson later in this section.
Real use case: hover effect with the mouse
const box = document.getElementById("box");
box.addEventListener("mouseenter", function () {
box.classList.add("hover");
});
box.addEventListener("mouseleave", function () {
box.classList.remove("hover");
});
For simple colour changes, CSS :hover is better. Use JavaScript hover events when you need to do something, such as showing a tooltip with data.
Listener options
addEventListener takes an optional third argument. It can be a simple true / false (for capturing, explained in the next lesson) or an options object.
element.addEventListener("click", handler, { once: true, passive: true });
| Option | Meaning |
|---|---|
once: true |
Run the listener one time, then remove it automatically |
passive: true |
Promise the browser you will not call preventDefault(), so it can scroll smoothly |
capture: true |
Listen during the capturing phase instead of the bubbling phase (see the propagation lesson) |
signal |
Pass an AbortSignal so you can remove the listener (or many listeners) later |
once: run only one time
Perfect for “claim your bonus”, “accept the cookie banner” or “start the game”:
const bonusBtn = document.getElementById("bonus-btn");
bonusBtn.addEventListener(
"click",
function () {
console.log("Bonus claimed!");
},
{ once: true }
);
// Click as many times as you like: it prints only once.
passive: smoother scrolling
For events like scroll, wheel and touchstart, the browser normally has to wait and see whether your function calls preventDefault() before it scrolls. A passive listener promises it will not, so scrolling stays smooth:
window.addEventListener(
"scroll",
function () {
console.log("Scrolled to", window.scrollY);
},
{ passive: true }
);
signal: remove listeners with an AbortController
An AbortController creates a signal. Pass that signal to as many listeners as you like. When you call abort(), all of them are removed at once. No need to keep a reference to every function.
const controller = new AbortController();
const likeBtn = document.getElementById("like-btn");
const bio = document.getElementById("bio");
likeBtn.addEventListener("click", () => console.log("liked"), { signal: controller.signal });
bio.addEventListener("input", () => console.log("typing"), { signal: controller.signal });
// Later, for example when a popup closes:
controller.abort(); // both listeners are gone
This is very handy for popups, modals and widgets that need a clean “tear down”.
removeEventListener: stop listening
Sometimes you want a listener to stop working, for example after a game ends or when a popup closes. You use removeEventListener with the same three things you used to add it:
element.removeEventListener("event-name", the-same-function);
The key word is same. The function you remove must be the exact same function (the same reference), not one that merely looks the same.
const pingBtn = document.getElementById("ping-btn");
const stopBtn = document.getElementById("stop-btn");
const log = document.getElementById("log");
function ping() {
log.textContent += "ping ";
}
pingBtn.addEventListener("click", ping);
stopBtn.addEventListener("click", function () {
pingBtn.removeEventListener("click", ping); // works: same function
log.textContent += "(stopped) ";
});
Click Try it, press Ping a few times, then press Stop pinging. The Ping button no longer reacts.
Why anonymous functions cannot be removed
This does not work, even though the two functions look identical:
const pingBtn = document.getElementById("ping-btn");
pingBtn.addEventListener("click", function () {
console.log("ping");
});
// A brand new function object: the browser finds nothing to remove
pingBtn.removeEventListener("click", function () {
console.log("ping");
});
Every time JavaScript sees function () { ... } it creates a new function. Two functions are only “the same” if they are literally the same object. This links back to Value vs Reference: functions are reference values. If you plan to remove a listener, give the function a name (or store it in a variable), or use the once or signal option instead.
Options must match when removing
If you added a listener with capture: true, you must remove it with capture: true too. Otherwise the browser treats it as a different listener:
const box = document.getElementById("box");
function show() {
console.log("hi");
}
box.addEventListener("click", show, true);
box.removeEventListener("click", show); // does NOT remove it
box.removeEventListener("click", show, true); // removes it
Other options (once, passive) do not matter when removing. Only capture does.
this inside a listener
In a regular function, this is the element the listener is attached to (the same as event.currentTarget). In an arrow function, this is not the element. It is whatever this was outside the arrow.
const likeBtn = document.getElementById("like-btn");
likeBtn.addEventListener("click", function () {
console.log(this.id); // "like-btn"
});
likeBtn.addEventListener("click", () => {
console.log(this === likeBtn); // false
});
The safe habit: use event.currentTarget (or the variable you already have) instead of this. It works the same with both kinds of function. If this feels confusing, review The this Keyword.
Passing your own values to a listener
The browser decides what is passed to your function (the event). To use your own values, wrap the call in an arrow function:
const likeBtn = document.getElementById("like-btn");
function greet(name) {
console.log("Hello, " + name);
}
// Wrong: runs immediately, when the page loads
// likeBtn.addEventListener("click", greet("Riya"));
// Right: runs on click
likeBtn.addEventListener("click", () => greet("Riya"));
onclick vs addEventListener
You may see older code that uses element.onclick = function () {...} or even <button onclick="doThing()">. They work, but addEventListener is better.
element.onclick = fn |
element.addEventListener("click", fn) |
|
|---|---|---|
| Multiple handlers | No. A new one replaces the old one | Yes. Each call adds one |
| Remove a handler | element.onclick = null |
removeEventListener |
Options (once, passive, signal) |
No | Yes |
| Works for every event type | Only events that have an on... property |
Yes |
| Keeps HTML and JavaScript separate | Property form: yes. HTML attribute form: no | Yes |
const likeBtn = document.getElementById("like-btn");
likeBtn.onclick = function () { console.log("A"); };
likeBtn.onclick = function () { console.log("B"); }; // replaces A
// One click prints only: B
Use addEventListener in all new code.
Wait for the page to be ready
A listener can only be added to an element that exists. If your script runs before the HTML is built, getElementById returns null and you get Cannot read properties of null (reading 'addEventListener').
Two fixes you already know: put the script at the end of <body>, or add defer to the script tag. A third option is to wait for the DOMContentLoaded event:
document.addEventListener("DOMContentLoaded", function () {
const likeBtn = document.getElementById("like-btn");
likeBtn.addEventListener("click", () => console.log("ready and clicked"));
});
Common mistakes
- Calling the function instead of passing it.
addEventListener("click", doIt())runsdoItat once. WriteaddEventListener("click", doIt), or() => doIt(). - Writing
"onclick"as the event name. It is"click". Theonprefix is only for the old property style. - Removing an anonymous function.
removeEventListenerneeds the exact same function. Give it a name, or useonce/signal. - Forgetting that
capturemust match when removing a listener that was added withcapture: true. - Adding a listener to
null. The element was not found (typo, wrong id, or script ran too early). Check the selector, or move the script to the end of<body>. - Adding the same listener again and again. Calling
addEventListenerinside another event handler stacks up listeners, so one click later runs the code many times. Add it once, outside. (The browser ignores an exact duplicate of the same named function with the same options, but anonymous functions are always new.) - Using
clickfor form submission. Pressing Enter in a field submits a form without clicking the button. Listen to the form’ssubmitevent instead. - Reading
event.target.valueon the wrong element.targetis the element the user touched.valueonly exists on inputs. - Using
changewhen you wantedinput. On a text box,changewaits until the field loses focus.inputfires on every keystroke. - Using an arrow function and then using
this.thisis not the element there. Useevent.currentTarget.
Practice
- Click Practice in Editor. Change the like button so it adds 2 likes per click.
- Add a second listener to the same like button that prints
"Thanks for the like!"in the console. Click once and check that both listeners run. - Make the like count turn red when it goes above
5. - Update the character counter so it shows how many characters are left (
100 - length), not how many are used. - Make the search result text show in capital letters as the user types.
- Add a
keydownlistener ondocumentthat printsevent.key. Press different keys and look at the values for Enter, Space and the arrow keys. - Use
mouseenterandmouseleaveto change the text of the box to"Thanks for visiting!"and back to"Move your mouse over me". - Make the Claim bonus button work only once using the
onceoption. Then try again usingremoveEventListenerinstead, and compare. - Change the Ping button so it stops itself after 3 pings by removing its own listener.
- Challenge: use one
AbortControllerto add three different listeners, and a Stop all button that removes all of them with a singleabort()call. - Challenge: build a click counter button that shows
"Clicked 3 times", and a Reset button that sets it back to zero.
Recap
- An event is something that happens on the page. A listener is a function that runs when it does.
- Use
element.addEventListener("click", handler). The event name has noon. Pass the function itself, nothandler(). - One element can have many listeners, and they run in the order you added them.
- The browser passes an event object to your function. Use
event.type,event.target,event.keyand more. - Common events:
click,input,change,submit,keydown,mouseenter,mouseleave,scroll,DOMContentLoaded. removeEventListenerneeds the same function reference (and the samecapturevalue). Anonymous functions cannot be removed.- Options:
once(run one time),passive(smooth scrolling),capture, andsignal(remove many listeners with oneabort()). - In a regular function
thisis the element. In an arrow function it is not. Preferevent.currentTarget. - Prefer
addEventListeneroveronclick. - Next you will learn event propagation: what happens when you click an element that sits inside other elements that also have listeners.