JavaScriptBeginner

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.

All JavaScript lessons

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);
  1. element: the thing you are listening to (found with getElementById, querySelector, and so on).
  2. "event-name": a string such as "click". There is no on in front of it.
  3. 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">&#128077; 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 () { ... }) and btn.addEventListener("click", () => { ... }). The only difference is how this behaves, 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()) runs doIt at once. Write addEventListener("click", doIt), or () => doIt().
  • Writing "onclick" as the event name. It is "click". The on prefix is only for the old property style.
  • Removing an anonymous function. removeEventListener needs the exact same function. Give it a name, or use once / signal.
  • Forgetting that capture must match when removing a listener that was added with capture: 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 addEventListener inside 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 click for form submission. Pressing Enter in a field submits a form without clicking the button. Listen to the form’s submit event instead.
  • Reading event.target.value on the wrong element. target is the element the user touched. value only exists on inputs.
  • Using change when you wanted input. On a text box, change waits until the field loses focus. input fires on every keystroke.
  • Using an arrow function and then using this. this is not the element there. Use event.currentTarget.

Practice

  1. Click Practice in Editor. Change the like button so it adds 2 likes per click.
  2. 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.
  3. Make the like count turn red when it goes above 5.
  4. Update the character counter so it shows how many characters are left (100 - length), not how many are used.
  5. Make the search result text show in capital letters as the user types.
  6. Add a keydown listener on document that prints event.key. Press different keys and look at the values for Enter, Space and the arrow keys.
  7. Use mouseenter and mouseleave to change the text of the box to "Thanks for visiting!" and back to "Move your mouse over me".
  8. Make the Claim bonus button work only once using the once option. Then try again using removeEventListener instead, and compare.
  9. Change the Ping button so it stops itself after 3 pings by removing its own listener.
  10. Challenge: use one AbortController to add three different listeners, and a Stop all button that removes all of them with a single abort() call.
  11. 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 no on. Pass the function itself, not handler().
  • 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.key and more.
  • Common events: click, input, change, submit, keydown, mouseenter, mouseleave, scroll, DOMContentLoaded.
  • removeEventListener needs the same function reference (and the same capture value). Anonymous functions cannot be removed.
  • Options: once (run one time), passive (smooth scrolling), capture, and signal (remove many listeners with one abort()).
  • In a regular function this is the element. In an arrow function it is not. Prefer event.currentTarget.
  • Prefer addEventListener over onclick.
  • Next you will learn event propagation: what happens when you click an element that sits inside other elements that also have listeners.