An event is something that happens on the page — a click, a key press, a form submission. JavaScript lets you run code when one occurs.
const button = document.querySelector("#save");
button.addEventListener("click", () => {
console.log("Saved");
});The pattern#
element.addEventListener("eventName", callbackFunction);Three parts: what to watch, what to watch for, and what to do. The function runs later, whenever the event happens.
The events you will actually use#
| Event | Fires when |
|---|---|
click |
Anything is clicked or tapped |
input |
A field’s value changes, on every keystroke |
change |
A field loses focus after changing, or a select changes |
submit |
A form is submitted |
keydown |
A key goes down |
DOMContentLoaded |
The HTML has finished parsing |
The event object#
Your callback receives an object describing what happened.
document.querySelector("#name").addEventListener("input", (event) => {
console.log(event.target.value); // what is currently typed
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeMenu();
});event.target is the element the event actually happened on. It is the most useful property on the object by far.
Forms: stopping the page reload#
By default, submitting a form reloads the page. That is almost never what you want in a JavaScript app.
const form = document.querySelector("#signup");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stop the reload
const email = form.querySelector("#email").value.trim();
if (!email.includes("@")) {
console.log("Please enter a valid email address");
return;
}
console.log("Submitting", email);
});Listen for submit on the form, not click on the button. That way pressing Enter in a field works too, which matters for keyboard users.
Event delegation#
A listener attached to a button that does not exist yet will never fire. Instead, listen on a parent that does exist and work out what was clicked.
const list = document.querySelector("#todo-list");
list.addEventListener("click", (event) => {
const deleteButton = event.target.closest(".delete");
if (!deleteButton) return; // clicked something else
deleteButton.closest("li").remove();
});One listener handles every item, including ones you add later. This is the standard solution for dynamic lists.
Bubbling#
Events travel upward from the element to its ancestors. That is what makes delegation work. Occasionally you need to stop it:
innerButton.addEventListener("click", (event) => {
event.stopPropagation(); // do not let the parent also react
});Use this sparingly — it can make behaviour hard to trace later.
Removing a listener#
function onScroll() { console.log("scrolling"); }
window.addEventListener("scroll", onScroll);
window.removeEventListener("scroll", onScroll); // needs the same referenceThis only works with a named function. An inline arrow function cannot be removed, because you no longer have a reference to it.
Questions people ask#
What is the difference between input and change?
input fires on every keystroke, which is what you want for live search or character counters. change fires once the field loses focus.
Why does my listener fire twice?
Usually the code that attaches it runs more than once. Attach listeners at setup, not inside a function you call repeatedly.
Should I use onclick in the HTML?
You will see <button onclick="doThing()"> in older tutorials. It works, but it mixes behaviour into your markup and only allows one handler. Prefer addEventListener.