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.
- 01Install listenersUse addEventListener with a type and a callback.
- 02Read event detailsUse target, currentTarget, timeStamp, isTrusted, and type-specific fields.
- 03Clean upRemove listeners and use once, passive, and signal options correctly.
What events are
A browser page is not just a picture. People click buttons, press keys, type text, move pointers, focus fields, scroll, resize the window, and sometimes your own code creates events too. An event is the browser’s announcement that one of those things happened.
A listener is a function you register for a specific event type. When that event is dispatched to the element, the browser calls your function and gives it an event object with details about the event. If you learned callbacks, this is the callback idea attached to the page.
Events are browser notifications. addEventListener says, “when this kind of notification happens on this element, call this function.”
Imagine a door with no bell. Visitors may arrive, but the house does not know. You install a bell for one kind of visit: “when someone presses this button, ring inside.” That is addEventListener: pick a page element, pick an event type, and give the browser a function to call.
- In real life: The door
- In JavaScript: A DOM element, like a button
- In real life: The kind of visit you care about
- In JavaScript: An event type, like
click - In real life: Installing the bell
- In JavaScript: Calling
addEventListener - In real life: The house responds when it rings
- In JavaScript: The browser calls your listener function
Where the analogy stops: A real doorbell rings for anyone at the door. DOM events have precise rules about targets, phases, default actions, and trust. This lesson starts with the listener; the next lesson goes deeper into travel through the page.
In this lesson you will inspect real event objects, fall into and fix the classic removeEventListener trap, try the once, passive, and signal options, and step through the exact order of listener registration and dispatch.
addEventListener: type, function, options
CORE APIThe method shape is element.addEventListener(type, listener, options). The type is a string such as "click", "keydown", or "input". The listener is a function (or a handler object) that the browser will call later. The third argument is optional: it can be an options object or the older boolean useCapture.
const button = document.querySelector("#save");const status = document.querySelector("#status"); button.addEventListener("click", (event) => { status.textContent = "Saved at " + Math.round(event.timeStamp) + "ms";});The important part: line 4 does not run the arrow function immediately. It stores it. Later, when the click happens, the browser calls it synchronously during event dispatch. If the listener changes text, classes, or state, that change happens before the browser returns from dispatching that event.
| Part | Example | Meaning |
|---|---|---|
| type | "click" | The name of the event to listen for. |
| listener | handleClick | The function or handler object to call later. |
| options | { once: true } | Extra rules such as one-time use, passive behavior, signal cleanup, or capture. |
You can add more than one listener to the same element and event type. They are independent unless they are the exact same registration: same event type, same listener object, and same capture value. Duplicate registrations with those three pieces equal are ignored.
The event object: the delivery slip
INTERACTIVEEvery listener receives an event object. It tells you what happened, where it happened, when it happened, whether it came from a real user action, and extra details for that kind of event. A key event has key and code. A mouse or pointer event has coordinates and a button.
When a package arrives, the slip says what it is, where it came from, and who signed for it. The event object is the browser’s slip. You usually do not need every field, but knowing where to look keeps listener code small.
- In real life: Package type
- In JavaScript:
event.type, such asclick - In real life: Delivery address
- In JavaScript:
event.target, where it happened - In real life: Desk currently processing it
- In JavaScript:
event.currentTarget, whose listener is running - In real life: Timestamp and signature
- In JavaScript:
timeStampandisTrusted
Where the analogy stops: A delivery slip is just paper. An event object is live during dispatch and also has methods such as preventDefault(). Some details depend on the event type.
Try the inspector. Turn event types on and off, then interact with the live controls. The fake click button dispatches new MouseEvent("click"), so the log shows isTrusted=false. Real user actions show isTrusted=true in the browser.
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.
Choose event types, then click, type, focus, or blur the controls.
isTrusted is false.Two fields are easy to mix up. event.target is where the event started. event.currentTarget is the element whose listener is currently running. In this lesson most listeners sit directly on the control they read; the next lesson, Bubbling & capturing, explores why these can differ.
removeEventListener needs the same key
INTERACTIVERemoving is exact: removeEventListener(type, listener, options) looks for a listener with the same event type, the same listener object, and the same capture value. Other options such as once and passive are not used for matching, but capture is.
If a building gives you a key, returning a different key that looks the same does not unlock the record. Inline arrows create a fresh function each time, so the browser cannot match the listener you meant to remove.
- In real life: The original key
- In JavaScript: The same function reference
- In real life: A copy cut to look similar
- In JavaScript: A new inline arrow
- In real life: Front-door vs back-door lock
- In JavaScript: The capture value must match
Where the analogy stops: Browsers are not judging what the function source text looks like. They compare object identity and capture, not whether two functions would do the same work.
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. Removal matches by event type, listener object, and capture value.
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 by whether the listener is really removed.
once, passive & signal options
INTERACTIVEListener options make cleanup and performance clearer. { once: true } removes a listener after the first call. { passive: true } promises that the listener will not cancel the browser’s default action. { signal } connects the listener to an AbortController so aborting the signal removes it. The Cancellation with AbortController lesson covers the same signal idea for asynchronous work.
Sometimes the bell should work once. Sometimes it should only observe traffic and never block it. Sometimes a building manager needs one switch that turns off several bells at once. Those are once, passive, and signal.
- In real life: A one-time voucher
- In JavaScript:
once: true - In real life: A promise not to block the road
- In JavaScript:
passive: true - In real life: One master switch for many bells
- In JavaScript:
signal: controller.signal
Where the analogy stops: Options do not change what a click or key event means. They change how the browser manages the listener around that event.
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 in the real browser.
once removes after one call, AbortController removes all signaled listeners, and passive click cancellation is ignored with defaultPrevented remaining false. Some browsers also default certain wheel and touch listeners on the page to passive.- Close a welcome popover after the first outside click
- Listen to scrolling or touch movement without blocking smooth scrolling
- Remove several listeners when a component unmounts
- Capture the first key a learner presses in a shortcut trainer
- Cancel pointermove, pointerup, and keydown listeners together
- Measure wheel events without ever cancelling them
Match each situation to the most helpful listener option.
In passive listeners, preventDefault() is ignored; browsers may log a warning. In some browsers, certain wheel, touchstart, and touchmove listeners on window, document, or body default to passive for scrolling performance, so state your intent with an options object when cancellation matters.
Registration and dispatch order
STEP THROUGHRegistration and dispatch are different moments. Registration stores a listener for later. Dispatch is the event happening now. During dispatch, listeners on the same element run synchronously in registration order. Adding the same function twice with the same type and capture value registers it once, not twice.
Step through registration first, then dispatch. Predict which listeners run and in what order.
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(" | "));Notice the this detail. A regular function listener receives this as event.currentTarget. An arrow listener keeps the outer this, so it is usually safer to read event.currentTarget. The this keyword lesson explains that binding rule in depth.
onclick, addEventListener, and handler objects
COMPAREYou will still see onclick. As a DOM property, it holds one handler. Assigning another function overwrites the previous one. addEventListener allows many independent listeners, which is why modern code uses it for most behavior. HTML attributes such as <button onclick="save()"> are discouraged because they mix markup and JavaScript and are harder to compose 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 handler, so the second assignment wins. addEventListener keeps adding independent listeners.
| Form | How many? | Use it? |
|---|---|---|
element.onclick = fn | One property value | Useful to recognize; easy to overwrite. |
element.addEventListener(type, fn) | Many listeners | Preferred for application code. |
element.addEventListener(type, object) | Many handler objects | Great when an object should handle several event types with handleEvent. |
onclick="..." in HTML | One inline attribute | Avoid in modern code; keep behavior in JavaScript. |
Where you will use events
Events are the front-end glue for real interfaces. A menu opens on click. A search field updates suggestions on input. A keyboard shortcut listens for keydown. A drag tool listens for pointerdown, pointermove, and pointerup. A cleanup function removes listeners when a widget disappears.
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();}This pattern is especially useful next to UI libraries. Let the framework render its own elements, but attach real DOM listeners in an effect or setup step, and remove them in cleanup. If you create DOM nodes by hand, keep them inside a container the framework does not manage.
Common misconceptions
- “addEventListener runs my function now.” It registers the function. Dispatch calls it later.
- “Two identical arrows are the same listener.” They are two different function objects.
- “target and currentTarget are always equal.” They match in simple cases, but differ when an event starts inside another element’s listener.
- “passive just makes code faster.” Passive is a promise not to cancel default behavior; speed comes from letting the browser proceed.
- “dispatchEvent creates a real user event.” Script-created events are useful for code paths, but
isTrustedis false. - “onclick is the same as addEventListener.” The property holds one handler; addEventListener allows many.
| Idea | Means | Remember |
|---|---|---|
target | Where the event began | The original delivery address. |
currentTarget | Whose listener is running now | The desk currently processing the slip. |
once | Remove after first call | One-time voucher. |
signal | Remove when aborted | Master switch. |
passive | Do not block default behavior | Promise not to block the road. |
Practice exercises
5 EXERCISESAssume button is a real DOM button. What does the final 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 registration is ignored because it is the same event type, same function object, and same capture value.
What concept fixes the bug? Type the missing word: keep a 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 the same function reference and pass that reference to both calls. Then the browser can match and remove it.
Write a setup pattern for three listeners that can be removed together.
// 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();Every listener is tied to the same AbortSignal. Aborting it removes all three listeners, so cleanup does not need three matching 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 can hold only one value, so the second function overwrites the first one.
Complete the listener in your browser console or 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 carries the details. Use currentTarget for the element whose listener is running, and pointer coordinates for where the pointer was in the viewport.
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 currently running?
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 7What is true about a passive listener that calls
preventDefault()?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)registers code for a future event.- The event object tells you the type, target, currentTarget, timing, trust, and type-specific details.
removeEventListenerneeds the same listener object and the same capture value.onceremoves after one call,passiverefuses cancellation, andsignalremoves on abort.- Property handlers hold one function;
addEventListenercomposes many listeners.
An event listener is a function the browser calls when a named event is dispatched to an element.
Up next: Bubbling & capturing.