JavaScript Events: Handling Clicks, Keyboard and Input with addEventListener
We listen to click, input, keydown and submit events with addEventListener, and learn about target and key on the event object, preventDefault, removeEventListener and why inline onclick is discouraged.
Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).
Predict First
Save this file, open it and keep the console open:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Predict</title>
</head>
<body>
<button id="button">Click me</button>
<script>
function greet() {
console.log("Hello!");
}
const button = document.getElementById("button");
button.addEventListener("click", greet());
</script>
</body>
</html>
Two questions: when the page opens, before you touch anything, what do you see in the console? And what happens when you click the button three times? Make a guess before you keep reading. The answer is in the section where we talk about what addEventListener expects.
What Is an Event?
Think of a doorbell. You don't stand at the door all day waiting to see if someone shows up. When the bell rings, you get up and open the door. Events work exactly like that. The browser watches everything that happens on the page: a click, a key press, someone typing into a box, a form being sent. Each of these is called an event. You tell the browser, "if this event happens on this element, run this function."
That function is the very callback we first met in the post on functions. You don't call it yourself; you hand it to the browser, and the browser calls it when the time comes. Your code no longer runs once from top to bottom and stops; it comes back to life every time the user does something.
Listening with addEventListener
Let's start with a counter. Save it, open it and click the button a few times:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Counter</title>
</head>
<body>
<button id="increase">Increase</button>
<p>Clicks: <span id="count">0</span></p>
<script>
const button = document.getElementById("increase");
const countArea = document.getElementById("count");
let counter = 0;
function increase() {
counter = counter + 1;
countArea.textContent = counter;
}
button.addEventListener("click", increase);
button.addEventListener("click", () => console.log("Clicked"));
</script>
</body>
</html>
addEventListener brings three things together: the element being listened to (button), the name of the event ("click") and the function to run when the event happens. You can add as many listeners as you like to the same element; above, a single click both increases the number and logs a line to the console. Listeners run in the order they were added.
Now for the answer to the opening question. Notice that in the counter example we passed the function without parentheses: increase. In the prediction example we wrote greet(). The moment you add parentheses, you call the function right there on that line. That's why you see "Hello!" once in the console as soon as the page opens. Since greet returns nothing, the result is undefined, and addEventListener receives undefined instead of a function. The browser doesn't throw an error; it just has nothing to listen with. Click three times and nothing else appears in the console. The rule: don't call the function, hand over the function itself.
The Event Object: What Happened, and Where?
When the browser calls your listener, it hands it something extra: as the first parameter, an event object carrying all the details of what happened.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Event Object</title>
</head>
<body>
<button class="color">Red</button>
<button class="color">Green</button>
<button class="color">Blue</button>
<p id="choice">No choice yet.</p>
<script>
const choice = document.getElementById("choice");
function pickColor(event) {
console.log(event.type); // "click"
console.log(event.target); // the clicked button
choice.textContent = "You picked: " + event.target.textContent;
}
document.querySelectorAll(".color").forEach((button) => {
button.addEventListener("click", pickColor);
});
</script>
</body>
</html>
The three buttons share one function, yet we know which one was clicked thanks to event.target. target is the element where the event happened. event.type gives you the event's name. The parameter's name doesn't matter; you'll see event, e or evt in code, and they're all the same object.
Details like where on the screen the click happened (clientX, clientY) or which key was pressed live on this object too. How an event travels from one element to another on the page, and target's close relative currentTarget, are things we'll cover in detail in the Intermediate series.
Keyboard and Typing: input and keydown
input: On every change
The most useful event for listening to a text box is input. It runs whenever the box's value changes, that is, every time a letter is typed or deleted:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live Preview</title>
</head>
<body>
<input id="message" placeholder="Type something...">
<p id="preview"></p>
<p id="counter">0 / 50</p>
<script>
const message = document.getElementById("message");
const preview = document.getElementById("preview");
const counter = document.getElementById("counter");
message.addEventListener("input", (event) => {
const text = event.target.value;
preview.textContent = text;
counter.textContent = text.length + " / 50";
});
message.addEventListener("keydown", (event) => {
console.log("Key pressed:", event.key);
if (event.key === "Escape") {
message.value = "";
preview.textContent = "";
counter.textContent = "0 / 50";
}
});
</script>
</body>
</html>
As you type, the preview and the character count update instantly. The text inside the box comes from the value property. Notice we write the preview with textContent; we don't ignore the XSS risk we saw earlier here either. There's also a change event, which runs once when the user leaves the box and commits the value. Use input if you want a live reaction, change if the final value is enough.
keydown: Which key was pressed?
keydown runs the moment a key is pressed. event.key tells you which one: "a" or "A" for letters, and names like "Enter", "Escape", "ArrowUp" or "Backspace" for special keys. In the example above, pressing Escape clears the box. Watch the console output to learn the names of a few keys.
If you want a shortcut that works across the whole page, add the listener to document: document.addEventListener("keydown", ...). There's also event.code for the physical key regardless of keyboard layout, but event.key is enough for most jobs. The keypress event you see in older code is deprecated; use keydown instead.
Stopping the Default Behavior: preventDefault
Some elements come with built-in behavior from the browser. Clicking a link takes you to another page; submitting a form reloads the page. event.preventDefault() cancels that default behavior:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>preventDefault</title>
</head>
<body>
<a id="link" href="https://developer.mozilla.org">Go to MDN</a>
<form id="form">
<input id="name" placeholder="Your name">
<button>Send</button>
</form>
<p id="result"></p>
<script>
document.getElementById("link").addEventListener("click", (event) => {
event.preventDefault();
console.log("The link was clicked but the page didn't change.");
});
document.getElementById("form").addEventListener("submit", (event) => {
event.preventDefault();
const name = document.getElementById("name").value;
document.getElementById("result").textContent = "Hello, " + name;
});
</script>
</body>
</html>
Notice that we added the submit listener to the form, not to the button. That way the same code runs whether the user clicks the button or presses Enter inside the box. Without preventDefault, the page would reload and your message wouldn't show up for even a moment. We'll leave the full details of working with forms to the next post.
Stopping Listening: removeEventListener
When you no longer need a listener, you can remove it. Let's allow only three votes on this button:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Three Votes</title>
</head>
<body>
<button id="vote">Vote</button>
<script>
const voteButton = document.getElementById("vote");
let votes = 0;
function vote() {
votes = votes + 1;
console.log("Votes:", votes);
if (votes === 3) {
voteButton.removeEventListener("click", vote);
voteButton.textContent = "No votes left";
}
}
voteButton.addEventListener("click", vote);
</script>
</body>
</html>
For removeEventListener to work, you must give it the same function you gave when adding the listener. By "the same" I don't mean another function with identical code inside, I mean the same reference. The reference logic we saw in the post on objects applies here too: two separate functions that look identical are two separate objects. So if you plan to remove a listener later, write it as a named function or as a function stored in a variable. There's also a shortcut for one-time listeners; we'll see it in upcoming posts.
Common Pitfalls
Writing inline onclick
You'll see this a lot in older examples:
<button onclick="greet()">Click me</button>
It works, but it's not recommended. It mixes HTML and JavaScript together; to change the behavior you end up editing two files at once. The function it calls has to be global. Many sites have a security setting (Content Security Policy) that blocks inline code entirely. And the onclick property on the JavaScript side holds only one slot. Open the prediction file, paste this into the console, then click the button:
const btn = document.querySelector("button");
btn.onclick = () => console.log("First");
btn.onclick = () => console.log("Second"); // wipes out the first
btn.addEventListener("click", () => console.log("Third"));
btn.addEventListener("click", () => console.log("Fourth")); // both run
When you click, you'll see "Second", "Third" and "Fourth"; "First" has quietly disappeared. addEventListener doesn't overwrite, it adds.
Putting "on" in front of the event name
addEventListener("onclick", ...) doesn't throw an error, but it never runs, because there's no such event. The event's name is "click"; the on prefix is only used in HTML attributes and properties.
Adding a listener before the element exists
If the script runs before the element, getElementById returns null and null.addEventListener throws a TypeError. The fix we saw earlier applies here too: put the script at the end of the body or use defer.
A Small Experiment
★☆☆ Add a "Reset" button to the counter example. When clicked, the counter should go back to zero and the number on screen should update too.
★★☆ Predict before running: if you paste this into the console of the prediction file and click the button twice, how many lines do you see in the console? Then try it.
const btn = document.querySelector("button");
btn.addEventListener("click", () => console.log("Click"));
btn.removeEventListener("click", () => console.log("Click"));
★★★ Put a box (div) on the page. Move it by 10 pixels with the arrow keys ("ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"). Hint: add the listener to document, give the box position: relative, and update style.top and style.left with numbers you keep in variables.
We've learned to listen to the user with events. The place where users enter the most data into a page, though, is forms. In the next post we'll read the values of form fields, catch the submission and show the user clear error messages.