Introduction to events
Learn how addEventListener connects JavaScript to clicks, keys, input, and other browser events, how to read event objects, and how to remove listeners safely.
- 01Add listenersUse addEventListener to give the browser a function to call when an event happens.
- 02Read event factsRead type, target, currentTarget, timeStamp, isTrusted, and the extra facts a key or pointer event has.
- 03Clean upRemove listeners with the same function, and use the once, passive, and signal options.
What events are
A web page can react to people. They click buttons, press keys, type text, and move the mouse. Each of these things is an event. An event is the browser's message that something just happened. Your own code can create events too.
A listener is a function you give the browser for one kind of event on one element. When that event happens there, the browser calls your function. It also passes in an event object: an object full of facts about the event. If you know callbacks, a listener is a callback that the browser calls for you.
An event is the browser's message that something happened. addEventListener says: "When this kind of event happens on this element, call this function."
const log = [];button.addEventListener("click", () => log.push("listener"));log.push("before");button.dispatchEvent(new Event("click"));log.push("after");console.log(log.join(" > "));Line 1 makes an empty log array. Line 2 adds a listener to button. Line 3 puts "before" in the log.
Line 4 makes a click event with new Event("click") and hands it to the button with dispatchEvent. To dispatch an event means to deliver it and call its listeners. The browser calls the listener right away, before line 5 runs. So line 6 prints "before > listener > after".
Picture a door with no bell. Visitors come, but nobody inside knows. So you put in a bell. Now, when someone presses the button outside, it rings inside. The bell only rings when a visitor presses it, never before.
- In real life: The door
- In JavaScript: An element on the page, like a button
- In real life: The kind of visit you care about
- In JavaScript: An event type, like
click - In real life: Putting in the bell
- In JavaScript: Calling
addEventListener - In real life: The bell rings inside the house
- In JavaScript: The browser calls your listener function
const details = [];button.addEventListener("click", (event) => { details.push(event.type + ":" + event.currentTarget.id);});button.dispatchEvent(new Event("click"));console.log(details[0]);Line 2 adds a listener. When it runs, it gets the event object as event. Line 3 reads two facts from it:
event.typeis the event's name,"click".event.currentTarget.idis the id of the element that owns this listener,"save".
Line 5 sends a click, and line 6 prints "click:save".
In this lesson you will:
- look inside real event objects;
- see why
removeEventListenersometimes fails, and fix it; - try the
once,passive, andsignaloptions; - step through the order in which listeners are added and called.
addEventListener: type, function, options
CORE APIYou call it like this: element.addEventListener(type, listener, options). It takes three things:
typeis the event's name, as a string. For example,"click","keydown"(a key goes down), or"input"(the text in a field changes).listeneris the function the browser will call later. It can also be an object with ahandleEventmethod; you'll see that near the end.optionsis optional. It is usually an object of extra rules. Older code passestrueorfalsehere instead. That value is calleduseCapture.
const button = document.querySelector("#save");const status = document.querySelector("#status"); button.addEventListener("click", (event) => { status.textContent = "Saved at " + Math.round(event.timeStamp) + "ms";});Lines 1 and 2 find two elements on the page with document.querySelector. Line 4 adds the listener. It does not run the arrow function yet. It only stores it.
Later, when someone clicks the button, the browser calls the function. Line 5 then writes a message into status. event.timeStamp is the time of the click, in milliseconds since the page started loading. Math.round drops the decimals.
The listener runs to its end before the browser finishes the click. So any change it makes, such as new text, is already done when the click is over.
Each row of the table is one of the three things you pass in.
| What you pass | Example | What it does |
|---|---|---|
type | "click" | It names the event to listen for. |
listener | handleClick | It is the function (or handler object) the browser calls later. |
options | { once: true } | It adds extra rules: run only once, never stop the default action, remove on a signal, or capture (next lesson). |
You can add many listeners to the same element for the same event. Each one runs. There is one exception: if a new listener matches one that is already there, the browser ignores it. A match means all three of these are the same:
- the event type;
- the function (the very same function object, not just the same code);
- the capture value (
trueorfalse).
The event object: facts about the event
INTERACTIVEEvery listener gets an event object. It tells you:
- what happened:
type, such as"click"; - where it started:
target; - when it happened:
timeStamp; - whether a real person caused it:
isTrusted; - extra facts for that kind of event. A key event has
keyandcode. A mouse or pointer event has the pointer's position and which button was pressed. (A pointer is a mouse, a pen, or a finger.)
A key event has two names for the key:
keyis what the key typed, such as"A". Use it for text and for commands.codeis which physical key was pressed, such as"KeyA". It stays the same on every keyboard layout. Use it when the key's place matters, as in game controls.
currentTarget is only set while the listener runs. Afterwards it is null, so read it inside the listener.
A parcel arrives with a slip. The slip says what is inside, where it was sent, and who signed for it. You rarely read every line. You just look for the line you need.
- In real life: What is in the parcel
- In JavaScript:
event.type, such asclick - In real life: The address on the parcel
- In JavaScript:
event.target, where it started - In real life: The desk handling it now
- In JavaScript:
event.currentTarget, whose listener is running - In real life: The time and the signature
- In JavaScript:
timeStampandisTrusted
Try the inspector below. Tick the event types you want to watch. Then click, type, or press Tab to move through the controls. Each event adds a line to the log.
The "Dispatch a fake click" button makes its own click with new MouseEvent("click"). Code made that event, not a person, so the log shows trusted=false. Your own clicks show trusted=true.
const controls = document.querySelector("#inspector");const log = document.querySelector("#event-log"); function inspect(event) { const details = { type: event.type, target: event.target.id, currentTarget: event.currentTarget.id, timeStamp: Math.round(event.timeStamp), isTrusted: event.isTrusted, key: event.key, code: event.code, button: event.button, clientX: event.clientX, clientY: event.clientY, }; log.prepend(JSON.stringify(details));} controls.addEventListener("click", inspect);controls.addEventListener("keydown", inspect);controls.addEventListener("input", inspect);- No events yet.
Tick the event types to watch. Then click, type, or press Tab to move through the controls.
Two fields are easy to mix up:
event.targetis the element where the event started.event.currentTargetis the element whose listener is running now.
In this lesson, each listener sits on the element it reads, so the two are the same. The next lesson, Bubbling & capturing, shows when they differ.
button.addEventListener("click", (event) => { console.log(event.type + ":" + event.target.id + ":" + Math.round(event.timeStamp));});button.dispatchEvent(new Event("click"));Line 2 joins three facts with colons: the event's name, the id of the element where it started, and its time, rounded. Line 4 sends a click, so the listener prints "click:save:12".
"click:save:12".Here the time is a sample value, 12. When you run the code, you will see a different number, because a real time is new for every event. So compare two times instead of expecting one exact value.
removeEventListener needs the same function
INTERACTIVEremoveEventListener(type, listener, options) takes a listener off an element. It only removes a listener that matches exactly:
- the same event type;
- the same function object;
- the same capture value.
The other options, such as once and passive, don't count for the match. Only capture does.
() => save() makes a new function, so removal finds no match. Passing the same save to both calls works.A building gives you a key. When you leave, you must hand back that same key. A new key that only looks the same won't do. Each time the code runs, () => save() makes a brand-new function, like a new key. So the browser can't find the listener you meant to remove.
- In real life: The key you were given
- In JavaScript: The same function object
- In real life: A new key cut to look the same
- In JavaScript: A new inline arrow
- In real life: Front-door lock or back-door lock
- In JavaScript: The capture value must match
const button = document.querySelector("#alarm");let rings = 0; button.addEventListener("click", () => rings++);button.removeEventListener("click", () => rings++);button.click(); // rings is 1: the first arrow is still installed function ring() { rings++; }button.addEventListener("click", ring, { capture: true });button.removeEventListener("click", ring, { capture: false });button.click(); // rings is 3: capture did not matchbutton.removeEventListener("click", ring, { capture: true });Rings: 0. A listener is only removed when the type, the function, and the capture value all match.
let saves = 0;function save() { saves++;} button.addEventListener("click", save);button.removeEventListener("click", save);button.dispatchEvent(new Event("click"));console.log(saves);Line 2 creates one function, save. The name save is a reference to it: it points at that one exact function object. Line 6 adds it, and line 7 removes that same function.
Line 8 sends a click, but no listener is left to call. So saves stays at 0, and line 9 prints 0.
addEventListener("click", save); removeEventListener("click", save);addEventListener("click", () => save()); removeEventListener("click", () => save());addEventListener("click", save, true); removeEventListener("click", save, true);addEventListener("click", save, true); removeEventListener("click", save, false);- A
{ once: true }listener after its first event addEventListener("click", logger); removeEventListener("click", logger);wherelogger.handleEventexists
Sort each case: is the listener really removed, or does it keep listening?
once, passive & signal options
INTERACTIVEThe third argument, options, can hold three useful rules:
{ once: true }removes the listener after its first call.{ passive: true }is a promise: this listener will not stop the browser's default action. A default action is what the browser does on its own, such as scrolling the page or ticking a checkbox.{ signal }links the listener to anAbortController. AnAbortControlleris an object with asignaland anabort()method. Callingabort()removes every listener that got itssignal.
The Cancellation with AbortController lesson uses the same signal to stop other kinds of work.
Some bells should ring only once. Some should only watch the traffic and never block it. And sometimes a building manager wants one main switch that turns off many bells at once.
- In real life: A ticket you can use once
- In JavaScript:
once: true - In real life: A promise not to block the road
- In JavaScript:
passive: true - In real life: One main switch for many bells
- In JavaScript:
signal: controller.signal
let notices = 0;button.addEventListener("click", () => notices++, { once: true });button.dispatchEvent(new Event("click"));button.dispatchEvent(new Event("click"));console.log(notices);Line 2 adds a listener with { once: true }. Lines 3 and 4 send two clicks. The browser removes the listener right after the first one. So only one click counts, and line 5 prints 1. Use this for a welcome message or a tip you show only once.
const controller = new AbortController(); button.addEventListener("click", celebrate, { once: true });first.addEventListener("click", log, { signal: controller.signal });second.addEventListener("pointerdown", log, { signal: controller.signal });third.addEventListener("keydown", log, { signal: controller.signal }); checkbox.addEventListener("click", (event) => { event.preventDefault(); console.log(event.defaultPrevented);}, { passive: true }); controller.abort();once: 0 · signal: 0 · passive: 0
Try once, signal, and passive on real buttons.
- Close a welcome popup after the first click outside it
- Watch scrolling or finger movement without making scrolling jerky
- Remove several listeners when a part of the page is taken away
- Record the first key a learner presses in a shortcut trainer
- Remove pointermove, pointerup, and keydown listeners together
- Count wheel events without ever stopping them
Match each situation to the listener option that fits it best.
A passive listener can still call preventDefault(). Normally, preventDefault() stops the default action. In a passive listener, the browser ignores it and may print a warning in the console.
Some browsers make certain listeners passive on their own, so that scrolling stays smooth. This happens for wheel, touchstart, and touchmove listeners on window, document, or body. If your listener must stop the default action, say so: pass { passive: false }.
Once a signal is aborted, it stays aborted. Adding a listener with that old signal adds nothing. So make a new AbortController each time you build a new part of the page, and abort the old one when you clean up.
Registration and dispatch order
STEP THROUGHTwo moments matter:
- Registration is when
addEventListenerstores a listener for later. - Dispatch is when the event happens and the browser calls the listeners.
During dispatch, the listeners on one element run one after another, in the order they were added. Each one finishes before the next one starts. If you add the same function twice, with the same type and capture value, it is stored only once.
Lines 6 to 8 store listeners. Line 9 sends a click. Guess which listeners run, and in what order, then step through.
script
const button = document.querySelector("#save");function first(event) { log.push("first:" + (this === event.currentTarget)); }const second = (event) => log.push("second:" + (this === event.currentTarget)); button.addEventListener("click", first);button.addEventListener("click", second);button.addEventListener("click", first); // ignored: same type + function + capturebutton.dispatchEvent(new Event("click"));console.log(log.join(" | "));Look at this in the replay. In a regular function listener, this is event.currentTarget, the element. An arrow function has no this of its own. It uses the outer this, from the code around it.
So it is safer to read event.currentTarget. It works in both kinds of function. The this keyword lesson explains the full rule.
onclick, addEventListener, and handler objects
COMPAREYou will still see onclick in older code. It is a property on the element, and a property holds one value. So it holds one handler. Assigning a new function replaces the old one.
addEventListener can store many listeners side by side. That is why modern code uses it.
You may also see an HTML attribute such as <button onclick="save()">. Avoid it. It mixes JavaScript into the HTML, and it is hard to combine with other code safely.
const button = document.querySelector("button"); button.onclick = () => console.log("first property handler");button.onclick = () => console.log("second property handler"); button.addEventListener("click", () => console.log("listener A"));button.addEventListener("click", () => console.log("listener B")); const logger = { handleEvent(event) { console.log("object saw " + event.type); },};button.addEventListener("click", logger);The onclick property holds one function, so the second assignment wins. addEventListener keeps every listener you add.
const calls = [];const logger = { handleEvent(event) { calls.push(event.type); },};button.addEventListener("click", logger);button.dispatchEvent(new Event("click"));console.log(calls.join(", "));Line 2 creates an object with a method named handleEvent. That name is special. Line 7 passes the whole object to addEventListener. When line 8 sends the click, the browser calls the object's handleEvent method. So line 9 prints "click".
Keep the object in a variable if you want to remove it later. A new object would not match.
const calls = [];const card = {};card.onclick = () => calls.push("first");card.onclick = () => calls.push("second");card.onclick();console.log(calls.join(", "));Line 2 makes a plain object, card. Line 3 puts a function in its onclick property. Line 4 puts a second function there, which replaces the first. Line 5 calls the one that is left, so line 6 prints "second".
onclick property holds one function, so the second one replaces the first. addEventListener keeps every listener you add.When several parts of an app need to hear the same click, use addEventListener.
Each row of the table is one way to attach event code. Read across: how many handlers it can hold, and whether to use it.
| How you write it | How many can it hold? | Should you use it? |
|---|---|---|
element.onclick = fn | One function. | Know it when you see it. A second assignment replaces the first. |
element.addEventListener(type, fn) | Many functions. | Yes. Use this in your own code. |
element.addEventListener(type, object) | Many objects with a handleEvent method. | Yes, when one object should handle several event types. |
onclick="..." in HTML | One function, written in the HTML. | No. Keep event code in JavaScript. |
Where you will use events
Real pages use events everywhere:
- A menu opens on
click. - A search box shows suggestions on
input. - A keyboard shortcut listens for
keydown. - A drag tool listens for
pointerdown,pointermove, andpointerup. - A cleanup function removes listeners when a part of the page goes away.
function mountSearch(input, results) { const controller = new AbortController(); const { signal } = controller; input.addEventListener("input", updateResults, { signal }); input.addEventListener("keydown", closeOnEscape, { signal }); function updateResults(event) { results.textContent = "Searching for " + event.currentTarget.value; } function closeOnEscape(event) { if (event.key === "Escape") results.textContent = ""; } return () => controller.abort();}Line 2 creates one AbortController, and line 3 takes its signal. Lines 5 and 6 pass that signal to two listeners. Line 9 shows the search text as the user types. Line 13 clears the results when the user presses Escape.
Line 16 returns a cleanup function. Calling it aborts the controller, which removes both listeners at once.
This pattern also works next to UI libraries such as React. Let the library draw its own elements. Add real DOM listeners in its setup step (in React, an effect), and remove them in its cleanup step. If you create elements by hand, put them inside a container the library does not touch.
Keep the cleanup next to the setup. If you forget it, a hidden or replaced part of the page keeps its listeners. They still use old data, and the next time the part appears, the same work runs twice.
Common misconceptions
- "addEventListener runs my function now." It only stores the function. The browser calls it later, when the event happens.
- "Two arrows with the same code are the same listener." Each arrow is a separate function object, so they don't match.
- "target and currentTarget are always the same." They are the same when the event starts on the element that has the listener. They differ when it starts inside a child element; the next lesson shows how.
- "passive just makes code faster."
passiveis a promise not to stop the default action. The speed comes from that promise: the browser can scroll right away, without waiting for your listener. - "dispatchEvent creates a real user event." An event your code makes still calls the listeners, but its
isTrustedisfalse. - "onclick is the same as addEventListener." The
onclickproperty holds one function.addEventListenercan hold many.
Each row of the table is one word from this lesson. Read across: what it means, then an everyday comparison.
| Word | What it means | Everyday comparison |
|---|---|---|
target | The element where the event started. | The address on a parcel: where it was sent. |
currentTarget | The element whose listener is running now. | The desk that is handling the parcel right now. |
once | The listener is removed after its first call. | A ticket you can use one time. |
signal | The listener is removed when its controller aborts. | One main switch that turns off many lights. |
passive | The listener promises not to stop the default action. | A promise not to block the road. |
Practice exercises
5 EXERCISESAssume button is a real button on the page. What does the last line print?
const calls = [];
function save() { calls.push("save"); }
button.addEventListener("click", save);
button.addEventListener("click", save);
button.dispatchEvent(new Event("click"));
console.log(calls.length);It prints 1. The second addEventListener matches the first: same type, same function, same capture value. So the browser ignores it, and save runs once.
This listener is never removed. What fixes it? Type the missing word: keep a function ____. (It is the word for a name that points at one exact function.)
button.addEventListener("click", () => save());
button.removeEventListener("click", () => save());function handleClick(event) {
console.log(event.type);
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);Keep one function and pass that same function to both calls. Then the browser finds it and removes it.
Write setup code for three listeners that you can remove together with one call.
// Add click, input, and keydown listeners.
// Remove all of them later with one call.const controller = new AbortController();
const { signal } = controller;
button.addEventListener("click", saveDraft, { signal });
input.addEventListener("input", updatePreview, { signal });
window.addEventListener("keydown", closeOnEscape, { signal });
// Later, when the widget is removed:
controller.abort();All three listeners share one signal. Aborting it removes all three, so you don't need three separate removeEventListener calls.
What text prints?
const calls = [];
const button = {};
button.onclick = () => calls.push("first");
button.onclick = () => calls.push("second");
button.onclick();
console.log(calls.join(", "));It prints second. A property holds one value, so the second function replaces the first.
Fill in the listener so it logs the event type, the element's id, and the pointer position. Try it in your browser console or an editor.
const box = document.querySelector("#box");
box.addEventListener("pointerdown", (event) => {
// Fill in: log type, currentTarget id, and coordinates.
});const box = document.querySelector("#box");
box.addEventListener("pointerdown", (event) => {
console.log(event.type);
console.log(event.currentTarget.id);
console.log(event.clientX + ", " + event.clientY);
});The event object already holds these facts. currentTarget is the element whose listener is running. clientX and clientY say where the pointer was, measured from the top-left corner of the visible part of the page.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does
addEventListener("click", save)do?Choose an answer to see the explanation.
Question 2 of 7Which property is the element whose listener is running right now?
Choose an answer to see the explanation.
Question 3 of 7What does this print?
Read the code, then predictconst calls = []; function save() { calls.push("save"); } button.addEventListener("click", save); button.addEventListener("click", save); button.dispatchEvent(new Event("click")); console.log(calls.length);Choose an answer to see the explanation.
Question 4 of 7Why does this removal fail?
Read the code, then predictbutton.addEventListener("click", () => save()); button.removeEventListener("click", () => save());Choose an answer to see the explanation.
Question 5 of 7Which option removes a listener automatically after its first call?
Choose an answer to see the explanation.
Question 6 of 7A passive listener calls
preventDefault(). What happens?Choose an answer to see the explanation.
Question 7 of 7What does
isTrustedbecome forbutton.dispatchEvent(new Event("click"))?Choose an answer to see the explanation.
Key takeaways
addEventListener(type, listener, options)stores a function for an event that happens later.- The event object tells you the event's type, where it started (
target), whose listener is running (currentTarget), when it happened, whether a person caused it, and extra facts for that kind of event. removeEventListenerneeds the same function object and the same capture value.onceremoves a listener after one call.passivepromises not to stop the default action.signalremoves a listener when its controller aborts.- The
onclickproperty holds one function.addEventListenercan hold many.
An event listener is a function the browser calls when a named event happens on an element.
Up next: Bubbling & capturing.