completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Add listenersUse addEventListener to give the browser a function to call when an event happens.
  • 02
    Read event factsRead type, target, currentTarget, timeStamp, isTrusted, and the extra facts a key or pointer event has.
  • 03
    Clean 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.

The main idea

An event is the browser's message that something happened. addEventListener says: "When this kind of event happens on this element, call this function."

dispatchEvent runs the listener right awayPop out in the code editor (opens in a new tab)JavaScript
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".

Real-life analogyA listener is a doorbell

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
Read the event type and the elementPop out in the code editor (opens in a new tab)JavaScript
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.type is the event's name, "click".
  • event.currentTarget.id is 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 removeEventListener sometimes fails, and fix it;
  • try the once, passive, and signal options;
  • step through the order in which listeners are added and called.

addEventListener: type, function, options

CORE API

You call it like this: element.addEventListener(type, listener, options). It takes three things:

  • type is the event's name, as a string. For example, "click", "keydown" (a key goes down), or "input" (the text in a field changes).
  • listener is the function the browser will call later. It can also be an object with a handleEvent method; you'll see that near the end.
  • options is optional. It is usually an object of extra rules. Older code passes true or false here instead. That value is called useCapture.
A small click listenerPop out in the code editor (opens in a new tab)JavaScript
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.

Left: line 4 registers a listener. The button cart now holds a function card for click. It is stored but has not run. Right: later a click event arrives, an arrow leads to the listener function, and it runs now.line 4: registerclick: fnbuttonstored, not run yetlater: a clickclicklistenerthe eventruns now
Line 4 only stores the function on the button. The browser calls it later, each time a click happens.

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.

The three things you pass to addEventListener
What you passExampleWhat it does
type"click"It names the event to listen for.
listenerhandleClickIt 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 (true or false).

The event object: facts about the event

INTERACTIVE

Every 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 key and code. 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:

  • key is what the key typed, such as "A". Use it for text and for commands.
  • code is 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.

Real-life analogyThe event object is a delivery slip

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 as click
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: timeStamp and isTrusted

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.

Event inspector
Event inspector sourcePop out in the code editor (opens in a new tab)JavaScript
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);
Live DOM controls3 types
  1. No events yet.
Try it yourself

Tick the event types to watch. Then click, type, or press Tab to move through the controls.

The demo controls are made and given listeners with DOM methods. When the lab closes, every listener is removed. Code makes the fake click, not a person, so its log line shows trusted=false.

Two fields are easy to mix up:

  • event.target is the element where the event started.
  • event.currentTarget is 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.

Print three facts about a clickPop out in the code editor (opens in a new tab)JavaScript
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".

A cart called event holds three cards: type click, target save, and time 12. An arrow labelled line 2 leads to the printed text click:save:12.typetargettimeclick#save12eventline 2"click:save:12"
The event object holds facts about the click: its type, where it started, and when (12 is a sample time). Line 2 joins them into "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

INTERACTIVE

removeEventListener(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.

Left: add uses one inline arrow, () => save(), and remove uses another. They are two different functions, so the listener keeps listening. Right: add and remove both use the saved function save. It is the same function, so the listener is removed.two inline arrowsadd() => save()remove() => save()2 functionsstill listeningone saved functionaddsaveremovesavesame functionremoved
Each () => save() makes a new function, so removal finds no match. Passing the same save to both calls works.
Real-life analogyReturn the exact key

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
removeEventListener trap
A removal that fails, and one that worksPop out in the code editor (opens in a new tab)JavaScript
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 });
Alarm0 rings
    Try it yourself

    Rings: 0. A listener is only removed when the type, the function, and the capture value all match.

    This lab shows the mistake first. Reset puts in a new alarm button, so all old listeners are gone.
    Use the same function to add and removePop out in the code editor (opens in a new tab)JavaScript
    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.

    Will this listener be removed?
    • 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); where logger.handleEvent exists
    Try it yourself
    0 of 6 correct

    Sort each case: is the listener really removed, or does it keep listening?

    Choose a category for every card. You can change an answer at any time; Reset clears them all.

    once, passive & signal options

    INTERACTIVE

    The 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 an AbortController. An AbortController is an object with a signal and an abort() method. Calling abort() removes every listener that got its signal.

    The Cancellation with AbortController lesson uses the same signal to stop other kinds of work.

    Real-life analogyOptions are rules on the bell

    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
    once: the listener runs one timePop out in the code editor (opens in a new tab)JavaScript
    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.

    Options lab
    Three listener optionsPop out in the code editor (opens in a new tab)JavaScript
    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();
    Live options0 calls

    once: 0 · signal: 0 · passive: 0

    Try it yourself

    Try once, signal, and passive on real buttons.

    What to expect: the once button works one time. Abort removes all three signal listeners. The passive checkbox still ticks, because its preventDefault() is ignored. Some browsers also make certain wheel and touch listeners passive on their own.
    Which option would you use?
    • 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
    Try it yourself
    0 of 6 correct

    Match each situation to the listener option that fits it best.

    Choose a category for every card. You can change an answer at any time; Reset clears them all.

    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 THROUGH

    Two moments matter:

    • Registration is when addEventListener stores 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.

    Registration and dispatch order
    Step 0 of 8Ready
    Your turn: follow the blue line

    Lines 6 to 8 store listeners. Line 9 sends a click. Guess which listeners run, and in what order, then step through.

    Running in
    1. script
    Next: line 1
    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(" | "));
    CallStoreChangeResultBlue line: runs next. Ran: already executed.
    Recent returnsNothing yet. Start with the blue line.
    Change line 8
    A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

    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

    COMPARE

    You 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.

    onclick or addEventListener
    onclick, two listeners, and an objectPop out in the code editor (opens in a new tab)JavaScript
    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);
    Live button0 calls
      Try it yourself
      Click the live button. Reset clears the log.

      The onclick property holds one function, so the second assignment wins. addEventListener keeps every listener you add.

      HTML attributes such as onclick='...' are an older way. They mix code into the HTML, so this lesson does not use them.
      An object as a listenerPop out in the code editor (opens in a new tab)JavaScript
      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.

      onclick holds one functionPop out in the code editor (opens in a new tab)JavaScript
      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".

      Left: card.onclick is one box. It holds only the second function, because the second assignment replaced the first. Right: addEventListener keeps a cart of listeners. It holds both fn A and fn B.card.onclicksecondonclickone slot:second replaced firstaddEventListenerfn Afn Bbuttonboth are kept
      The 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.

      Ways to attach event code
      How you write itHow many can it hold?Should you use it?
      element.onclick = fnOne 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 HTMLOne 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, and pointerup.
      • A cleanup function removes listeners when a part of the page goes away.
      Set up a search box, with cleanupPop out in the code editor (opens in a new tab)JavaScript
      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." passive is 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 isTrusted is false.
      • "onclick is the same as addEventListener." The onclick property holds one function. addEventListener can hold many.

      Each row of the table is one word from this lesson. Read across: what it means, then an everyday comparison.

      Event words that are easy to mix up
      WordWhat it meansEveryday comparison
      targetThe element where the event started.The address on a parcel: where it was sent.
      currentTargetThe element whose listener is running now.The desk that is handling the parcel right now.
      onceThe listener is removed after its first call.A ticket you can use one time.
      signalThe listener is removed when its controller aborts.One main switch that turns off many lights.
      passiveThe listener promises not to stop the default action.A promise not to block the road.

      Practice exercises

      5 EXERCISES
      Exercise 1 · Warm-upPredict the repeated listener

      Assume button is a real button on the page. What does the last line print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const calls = [];
      function save() { calls.push("save"); }
      button.addEventListener("click", save);
      button.addEventListener("click", save);
      button.dispatchEvent(new Event("click"));
      console.log(calls.length);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 2 · PracticeFix a removal that does nothing

        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.)

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        button.addEventListener("click", () => save());
        button.removeEventListener("click", () => save());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 3 · PracticeRemove three listeners with one call

          Write setup code for three listeners that you can remove together with one call.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          // Add click, input, and keydown listeners.
          // Remove all of them later with one call.
            Exercise 4 · Warm-upTwo functions in onclick

            What text prints?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const calls = [];
            const button = {};
            button.onclick = () => calls.push("first");
            button.onclick = () => calls.push("second");
            button.onclick();
            console.log(calls.join(", "));

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 5 · ChallengeBuild a tiny inspector

              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.

              Starter codePop out in the code editor (opens in a new tab)JavaScript
              const box = document.querySelector("#box");
              box.addEventListener("pointerdown", (event) => {
                // Fill in: log type, currentTarget id, and coordinates.
              });

                Check your understanding

                7 QUESTIONS
                Introduction to events quiz · 7 questionsScore: first tries count
                1. Question 1 of 7What does addEventListener("click", save) do?

                  Choose an answer to see the explanation.

                2. Question 2 of 7Which property is the element whose listener is running right now?

                  Choose an answer to see the explanation.

                3. Question 3 of 7What does this print?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  const 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.

                4. Question 4 of 7Why does this removal fail?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  button.addEventListener("click", () => save());
                  button.removeEventListener("click", () => save());

                  Choose an answer to see the explanation.

                5. Question 5 of 7Which option removes a listener automatically after its first call?

                  Choose an answer to see the explanation.

                6. Question 6 of 7A passive listener calls preventDefault(). What happens?

                  Choose an answer to see the explanation.

                7. Question 7 of 7What does isTrusted become for button.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.
                • removeEventListener needs the same function object and the same capture value.
                • once removes a listener after one call. passive promises not to stop the default action. signal removes a listener when its controller aborts.
                • The onclick property holds one function. addEventListener can hold many.

                An event listener is a function the browser calls when a named event happens on an element.

                Up next: Bubbling & capturing.

                CompleteFrontend Clear concepts. Working examples.