PLAINFINCH learn by building

Events

You will make your page respond when someone clicks, presses a key or types in a box.

What is an event?

An event is the browser's announcement that something happened: a click, a key press, text typed into a box, a form being sent, the page finishing loading. Your script does not run all the time waiting for these. It registers functions in advance, and the browser calls them when the matching event happens.

You register with addEventListener(type, listener). The first argument is the event name as a string, such as "click" or "keydown", without the on prefix used by old HTML attributes. The second is a function. Pass the function itself, search, not the result of calling it, search(). With parentheses, the function runs once immediately and its return value, usually undefined, is what gets registered.

The Events lens in the editor lists every event on the page and which of your functions responded. It is the quickest way to answer the question "did my listener even run?"

const go = document.querySelector("#go");

function search() {
  console.log("Searching");
}

go.addEventListener("click", search);   // right: pass the function
// go.addEventListener("click", search()); // wrong: runs now, registers undefined
FigureA click on a button travels down from document through main and the list to the button in the capture phase, reaches the target, then bubbles back up through the list and main to document.A click on a button travels down from document through main and the list to the button in the capture phase, reaches the target, then bubbles back up through the list and main to document.
Every click travels down to the element you clicked (capture), arrives there (target), then travels back up through each ancestor (bubble). Ordinary listeners run on the way up, which is why a listener on a list can hear clicks on its buttons.

What is in the event object?

When the browser calls your listener, it passes one argument: an event object describing what happened. Name the parameter event or e; both are common. Some properties appear on every event, others only on certain types.

The difference between target and currentTarget trips up many people. If a button contains an icon in a <span> and the visitor clicks the icon, target is the span and currentTarget is the button. When you want the element you attached the listener to, use currentTarget.

const button = document.querySelector(".bird");
button.addEventListener("click", (event) => {
  console.log(event.type);                 // click
  console.log(event.target.tagName);       // SPAN if the icon was clicked
  console.log(event.currentTarget.tagName); // BUTTON every time
});
  • event.type is the event name, such as "click".
  • event.target is the element where the event started: the innermost thing that was clicked or typed into.
  • event.currentTarget is the element whose listener is running right now. Inside a listener added to a button, it is always that button.
  • event.key on keyboard events names the key: "Enter", "Escape", "a", "ArrowDown".
  • event.preventDefault() cancels the browser's normal reaction, covered below.

How do you handle the keyboard?

Listen for keydown. It fires when a key goes down and repeats while the key is held. keyup fires on release. The old keypress event is deprecated; it skips keys like Escape and the arrows, so do not use it in new code.

Read event.key to find out which key. It holds a readable name with exact capitalisation: "Enter", "Escape", " " for the space bar, and the typed character for letters, so Shift+A gives "A". You may see keyCode or which with numbers like 13 in older code; those are legacy and should be replaced.

Keyboard events go to the element that has focus. Buttons, links and form fields can take focus with Tab; a <div> cannot unless you add tabindex. This is one reason to use real <button> elements for anything clickable: a button already fires click when a keyboard user presses Enter or Space on it, so one click listener covers mouse, touch and keyboard. Try that in the demo by tabbing to the Search button and pressing Space.

const dialog = document.querySelector("#help");
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") {
    dialog.hidden = true;
  }
});

What is the difference between input and change?

Both fire on form fields, at different moments. input fires on every edit: each character typed, each paste, each deletion. change fires when the edit is committed, which for a text box means when it loses focus after its value changed, and for a checkbox, radio button or <select> means right after the choice is made.

Use input for live feedback: a character counter, a filter that narrows a list as you type, a preview. Use change when you only care about the final value, such as saving a setting. For checkboxes and selects the two fire together, so either works.

Read the current text from the field's value property inside the listener. event.target.value works too and is handy when one function listens to several fields.

const note = document.querySelector("#note");
const counter = document.querySelector("#count");

note.addEventListener("input", () => {
  counter.textContent = `${note.value.length} / 140`;
});

How do you stop the browser's default action?

Some events come with built-in behaviour. Clicking a link follows it. Submitting a form sends it and loads a new page. Pressing Space on a page scrolls it. event.preventDefault() cancels that behaviour for this one event, while your own code still runs.

The most common use is a form you want to handle in JavaScript; the forms lesson covers that properly. For links, ask first whether it should be a link at all. If clicking it does not go anywhere, a <button> is the right element, and there is no default to prevent.

Calling preventDefault does not stop the event from bubbling to ancestors. That is a separate method, stopPropagation, which you will rarely need and should use with care, because it hides the event from every listener further up.

const more = document.querySelector("a.more");
const extra = document.querySelector("#extra");

more.addEventListener("click", (event) => {
  event.preventDefault();   // stay on this page
  extra.hidden = false;
});

What does bubbling mean?

An event does not stay on the element where it started. After reaching the target, it travels up through every ancestor: the button's list item, the list, the main element, the body, the document. A click listener on any of those hears the click too. This upward trip is called bubbling.

Before bubbling there is a capture phase, where the event travels down from the document to the target. Listeners only run during capture if you ask for it with { capture: true }, which is rare. In everyday code you can think of it as: the event starts at the clicked element and rises.

Bubbling is what makes event delegation work, the topic of a later lesson: one listener on a list can handle clicks on every button inside it, including buttons added after the page loaded. Check the Events lens after clicking a button inside a list that has its own listener; both functions appear for the same click. A few events do not bubble, such as focus and blur; their bubbling versions are focusin and focusout.

document.querySelector("#birds").addEventListener("click", (event) => {
  console.log("List heard a click on", event.target.textContent);
});

How do you stop listening?

removeEventListener takes the same type and the same function you passed to addEventListener. It must be the very same function, not one that looks identical. An arrow function written inline cannot be removed later because you have no name to pass back, so give the function a name if you plan to remove it.

For a listener that should run only once, such as a one-time offer or a welcome message, pass { once: true } as the third argument. The browser removes the listener after its first run. Option names are not checked, so a typo like { one: true } is silently ignored and the listener keeps firing.

You also need to remove listeners when an element stays on the page but its job ends, for example a drag handler that should stop after the mouse is released.

const claim = document.querySelector("#claim");

function giveLoaf() {
  console.log("Free loaf claimed");
}

claim.addEventListener("click", giveLoaf, { once: true });
// or, by hand: claim.removeEventListener("click", giveLoaf);

What do AI tools get wrong with events?

Watch for onclick="..." attributes in generated HTML; they mix code into markup and can only hold one handler. Replace them with addEventListener in the script. Look also for listeners added to <div> or <span> elements acting as buttons, which keyboard users cannot reach, and for keyCode === 13 where event.key === "Enter" reads better.

Other common slips: calling the function when passing it (addEventListener("click", save())), using event.target where the code needs currentTarget, adding the same listener inside another listener so it piles up with every click, and forgetting preventDefault on form submits. When a click seems to do something twice, the Events lens shows how many functions ran; two entries for one click usually means the listener was added twice.

Field notes

  • An event is the browser telling your code that something happened, like a click or a key press.
  • addEventListener connects an event type to the function that should run.
  • The event object tells you details, such as which key was pressed in event.key.
  • event.preventDefault() stops built-in behaviour, like a form reloading the page.

Easy to get wrong

With parentheses, save runs immediately and its return value is registered instead of the function. Pass the function itself.

Event names have no on prefix. "onclick" is not an event, so the listener never runs and no error appears.

keypress and keyCode are legacy. keydown with event.key is current, readable and covers every key.

Two arrow functions with the same body are still two different functions, so nothing is removed. Pass the same named function to both calls.

Questions people ask

Should I name the parameter event or e?

Either. Pick one and stay consistent. event is clearer for beginners; e is shorter and very common in existing code.

Can one element have several listeners for the same event?

Yes. They run in the order they were added. That is one advantage over onclick properties, which hold only one function.

Why does my click listener on a div not work with the keyboard?

A <div> cannot receive focus or fire click from Enter and Space. Use a <button>, which does both without extra code.

Does preventDefault stop bubbling?

No. It cancels the browser's own action only. The event still bubbles to ancestors unless you call stopPropagation.

Where should I add listeners: at the top or bottom of the script?

After the elements exist and after the functions they call are defined. With the script at the end of the body or loaded with defer, the elements are ready when it runs.

Keep going