Skip to content

Event Handling

💡
Before you start

You need a web browser and a plain text editor — nothing else. No Node, no web server, no internet connection: everything below runs from two files sitting in a folder on your own computer. You do not need to know HTML in advance either, because step 1 gives you the page to work on, line for line. If you have never written any JavaScript at all, do Getting Started with JavaScript first — about fifteen minutes.

What Are Events?

Events are things that happen in the browser — a user clicks a button, types in a text field, moves the mouse, scrolls the page, or submits a form. JavaScript lets you listen for these events and run code in response.

Every interaction a user has with your webpage generates events. Event handling is what turns a static HTML page into an interactive application.

// A simple event example
const button = document.querySelector("#my-button");

button.addEventListener("click", function() {
    console.log("Button was clicked!");
});

The pattern is always the same: select an element, choose an event type, and provide a function to run when that event occurs.

Adding Event Listeners

The addEventListener method is the standard way to attach event handlers in JavaScript. It takes two required arguments: the event type (a string) and a callback function.

const element = document.querySelector("#target");

// Using a named function
function handleClick() {
    console.log("Clicked!");
}
element.addEventListener("click", handleClick);

// Using an anonymous function
element.addEventListener("click", function() {
    console.log("Clicked!");
});

// Using an arrow function
element.addEventListener("click", () => {
    console.log("Clicked!");
});

Removing Event Listeners

To remove an event listener, you must pass the same function reference that was used to add it. This is why named functions are useful.

function handleClick() {
    console.log("Clicked!");
}

// Add the listener
element.addEventListener("click", handleClick);

// Later, remove it
element.removeEventListener("click", handleClick);

// This will NOT work — different function reference
element.addEventListener("click", function() { console.log("Hi"); });
element.removeEventListener("click", function() { console.log("Hi"); });
// The two anonymous functions are different objects in memory
⚠️
Avoid inline event handlers

Older code uses inline HTML attributes like <button onclick="doSomething()">. This mixes JavaScript into your HTML, makes code harder to maintain, and only allows one handler per event. Always use addEventListener instead — it keeps your code organized and supports multiple listeners on the same event.

Common Event Types

Mouse Events

const box = document.querySelector(".box");

// Click (press and release)
box.addEventListener("click", () => console.log("Clicked"));

// Double-click
box.addEventListener("dblclick", () => console.log("Double-clicked"));

// Mouse enters the element
box.addEventListener("mouseenter", () => {
    box.style.backgroundColor = "#1a1a2e";
});

// Mouse leaves the element
box.addEventListener("mouseleave", () => {
    box.style.backgroundColor = "";
});

// Mouse moves over the element
box.addEventListener("mousemove", (event) => {
    console.log(`Mouse at: ${event.clientX}, ${event.clientY}`);
});

Keyboard Events

const input = document.querySelector("#search");

// Key is pressed down
input.addEventListener("keydown", (event) => {
    console.log(`Key pressed: ${event.key}`);

    if (event.key === "Enter") {
        console.log("Search submitted!");
    }

    if (event.key === "Escape") {
        input.value = "";
    }
});

// Key is released
input.addEventListener("keyup", (event) => {
    console.log(`Key released: ${event.key}`);
});

// Detect keyboard shortcuts
document.addEventListener("keydown", (event) => {
    if (event.ctrlKey && event.key === "s") {
        event.preventDefault(); // Prevent browser save dialog
        console.log("Custom save action!");
    }
});

Form Events

const form = document.querySelector("#signup-form");
const emailInput = document.querySelector("#email");

// Form submission
form.addEventListener("submit", (event) => {
    event.preventDefault(); // Stop the page from reloading
    console.log("Form submitted!");
});

// Input changes in real time
emailInput.addEventListener("input", (event) => {
    console.log("Current value:", event.target.value);
});

// Input gains focus
emailInput.addEventListener("focus", () => {
    emailInput.classList.add("focused");
});

// Input loses focus
emailInput.addEventListener("blur", () => {
    emailInput.classList.remove("focused");
});

// Select/dropdown changes
const dropdown = document.querySelector("#country");
dropdown.addEventListener("change", (event) => {
    console.log("Selected:", event.target.value);
});

Window and Document Events

// Page has fully loaded
window.addEventListener("load", () => {
    console.log("Page fully loaded (images, styles, etc.)");
});

// DOM is ready (faster than load)
document.addEventListener("DOMContentLoaded", () => {
    console.log("DOM is ready");
});

// User scrolls the page
window.addEventListener("scroll", () => {
    console.log("Scroll position:", window.scrollY);
});

// Browser window is resized
window.addEventListener("resize", () => {
    console.log(`Window size: ${window.innerWidth}x${window.innerHeight}`);
});

The Event Object

Every event handler receives an event object as its first argument. This object contains detailed information about what happened.

document.addEventListener("click", function(event) {
    // What type of event occurred
    console.log(event.type);          // "click"

    // Which element was clicked
    console.log(event.target);        // The actual element clicked
    console.log(event.currentTarget); // The element the listener is on

    // Mouse position
    console.log(event.clientX);       // X position in viewport
    console.log(event.clientY);       // Y position in viewport
    console.log(event.pageX);         // X position in document
    console.log(event.pageY);         // Y position in document

    // Modifier keys held during click
    console.log(event.shiftKey);      // true if Shift was held
    console.log(event.ctrlKey);       // true if Ctrl was held
    console.log(event.altKey);        // true if Alt was held

    // Timestamp
    console.log(event.timeStamp);     // Milliseconds since page load
});
// Keyboard event properties
document.addEventListener("keydown", function(event) {
    console.log(event.key);       // "a", "Enter", "ArrowUp", etc.
    console.log(event.code);      // "KeyA", "Enter", "ArrowUp" (physical key)
    console.log(event.repeat);    // true if key is held down
});

Event Propagation

When you click on an element, the event does not just fire on that element alone. It travels through the DOM tree in two phases: capturing (going down) and bubbling (going back up).

// Given this HTML:
// <div id="outer">
//   <div id="inner">
//     <button id="btn">Click Me</button>
//   </div>
// </div>

document.querySelector("#outer").addEventListener("click", () => {
    console.log("Outer div clicked");
});

document.querySelector("#inner").addEventListener("click", () => {
    console.log("Inner div clicked");
});

document.querySelector("#btn").addEventListener("click", () => {
    console.log("Button clicked");
});

// Clicking the button logs (in bubbling order):
// "Button clicked"
// "Inner div clicked"
// "Outer div clicked"

Stopping Propagation

document.querySelector("#btn").addEventListener("click", (event) => {
    console.log("Button clicked");
    event.stopPropagation(); // Stops the event from bubbling up
});

// Now clicking the button only logs "Button clicked"
// The outer and inner handlers will NOT fire

Capturing Phase

// Listen during the capture phase (top-down) instead of bubbling
document.querySelector("#outer").addEventListener("click", () => {
    console.log("Outer - capture phase");
}, true); // Third argument: true = capture phase

// Now the order is:
// "Outer - capture phase"  (capture, going down)
// "Button clicked"         (target)
// "Inner div clicked"      (bubble, going up)

Event Delegation

Event delegation is a pattern where you attach a single event listener to a parent element instead of adding listeners to every child. The event bubbles up from the clicked child to the parent, where your handler catches it.

// Instead of adding a listener to EVERY list item...
// ❌ Inefficient approach
const items = document.querySelectorAll("li");
items.forEach(item => {
    item.addEventListener("click", function() {
        console.log(this.textContent);
    });
});

// ✅ Use delegation — one listener on the parent
const list = document.querySelector("ul");
list.addEventListener("click", function(event) {
    // Check if the clicked element is actually an <li>
    if (event.target.tagName === "LI") {
        console.log(event.target.textContent);
    }
});
💡
Why event delegation is powerful

Delegation uses less memory (one listener vs many), automatically handles dynamically added elements, and simplifies your code. If you add a new <li> to the list later, it will already be handled by the parent's listener — no extra setup needed.

// Delegation with closest() for complex elements
// HTML: <ul id="todo-list">
//   <li class="todo-item">
//     <span>Buy groceries</span>
//     <button class="delete-btn">X</button>
//   </li>
// </ul>

document.querySelector("#todo-list").addEventListener("click", (event) => {
    // Find the delete button even if a child element was clicked
    const deleteBtn = event.target.closest(".delete-btn");
    if (deleteBtn) {
        const todoItem = deleteBtn.closest(".todo-item");
        todoItem.remove();
        return;
    }

    // Find the todo item for toggling
    const todoItem = event.target.closest(".todo-item");
    if (todoItem) {
        todoItem.classList.toggle("completed");
    }
});

Preventing Default Behavior

Many HTML elements have built-in behaviors — links navigate to URLs, forms reload the page on submission, right-click opens a context menu. You can prevent these defaults with event.preventDefault().

// Prevent a link from navigating
const link = document.querySelector("a");
link.addEventListener("click", (event) => {
    event.preventDefault();
    console.log("Link click intercepted — navigating with JavaScript instead");
    // Custom navigation logic here
});

// Prevent form submission (handle it with JavaScript)
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
    event.preventDefault();
    // Validate and submit via fetch() instead
    console.log("Form handled by JavaScript");
});

// Prevent right-click context menu
document.addEventListener("contextmenu", (event) => {
    event.preventDefault();
    // Show custom context menu instead
});

Practical Example: Form Validation

Let us build a complete form validation system using everything covered in this tutorial.

// HTML:
// <form id="register-form">
//   <div class="form-group">
//     <label for="username">Username</label>
//     <input type="text" id="username" name="username">
//     <span class="error" id="username-error"></span>
//   </div>
//   <div class="form-group">
//     <label for="email">Email</label>
//     <input type="email" id="email" name="email">
//     <span class="error" id="email-error"></span>
//   </div>
//   <div class="form-group">
//     <label for="password">Password</label>
//     <input type="password" id="password" name="password">
//     <span class="error" id="password-error"></span>
//   </div>
//   <button type="submit">Register</button>
// </form>

const form = document.querySelector("#register-form");

// Validation functions
function validateUsername(value) {
    if (value.length === 0) return "Username is required";
    if (value.length < 3) return "Username must be at least 3 characters";
    if (value.length > 20) return "Username must be 20 characters or less";
    if (!/^[a-zA-Z0-9_]+$/.test(value)) return "Only letters, numbers, and underscores";
    return "";
}

function validateEmail(value) {
    if (value.length === 0) return "Email is required";
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "Enter a valid email address";
    return "";
}

function validatePassword(value) {
    if (value.length === 0) return "Password is required";
    if (value.length < 8) return "Password must be at least 8 characters";
    if (!/[A-Z]/.test(value)) return "Include at least one uppercase letter";
    if (!/[0-9]/.test(value)) return "Include at least one number";
    return "";
}

// Show or clear error message
function showError(inputId, message) {
    const errorSpan = document.querySelector(`#${inputId}-error`);
    const input = document.querySelector(`#${inputId}`);
    errorSpan.textContent = message;
    if (message) {
        input.classList.add("invalid");
        input.classList.remove("valid");
    } else {
        input.classList.remove("invalid");
        input.classList.add("valid");
    }
}

// Real-time validation using event delegation on the form
form.addEventListener("input", (event) => {
    const { id, value } = event.target;

    switch (id) {
        case "username":
            showError("username", validateUsername(value));
            break;
        case "email":
            showError("email", validateEmail(value));
            break;
        case "password":
            showError("password", validatePassword(value));
            break;
    }
});

// Form submission
form.addEventListener("submit", (event) => {
    event.preventDefault();

    const username = document.querySelector("#username").value;
    const email = document.querySelector("#email").value;
    const password = document.querySelector("#password").value;

    // Validate all fields
    const errors = {
        username: validateUsername(username),
        email: validateEmail(email),
        password: validatePassword(password)
    };

    // Show all errors
    Object.keys(errors).forEach(field => {
        showError(field, errors[field]);
    });

    // Check if form is valid
    const isValid = Object.values(errors).every(error => error === "");

    if (isValid) {
        console.log("Form is valid! Submitting...", { username, email });
        // Send data to server with fetch()
    } else {
        console.log("Form has errors");
        // Focus the first invalid field
        const firstError = Object.keys(errors).find(key => errors[key] !== "");
        document.querySelector(`#${firstError}`).focus();
    }
});

Now Do It Yourself: Five Steps

Five steps, one page, and by the end you will have written the pattern that real applications actually use: a single listener that handles a whole list, including the parts of it that did not exist when the listener was attached. Two files in one folder, opened straight from your disk — no server and no internet.

One honest note before you start. Everywhere else on this site the “you should see” lines were captured by running the code on the machine where the page was written. That was not possible here: events only happen inside a browser, and no browser could be opened there. Every snippet below was syntax-checked, and everything described is ordinary, standard browser behaviour — but if your browser ever disagrees with a line below, trust your browser.

1
Build a page with something to click

Go: make a new folder and open your text editor. On Windows use Notepad, on macOS TextEdit (with Format → Make Plain Text), on Linux whatever your desktop provides.

Do: save these lines as events-demo.html. In the Save dialog set Save as type to All Files first, or you will silently get events-demo.html.txt.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Events practice</title>
  <style>
    body { font-family: sans-serif; margin: 2rem; }
    li { cursor: pointer; }
    .done { text-decoration: line-through; color: grey; }
  </style>
</head>
<body>
  <h1>Shopping list</h1>
  <button id="clear">Clear the message</button>
  <p id="message">Nothing has happened yet.</p>
  <ul id="list">
    <li>bread</li>
    <li>milk</li>
  </ul>
  <form id="add-form">
    <input id="item" name="item" placeholder="something else">
    <button type="submit">Add</button>
  </form>
  <script src="events.js" defer></script>
</body>
</html>

Do: save an empty file called events.js beside it, then double-click events-demo.html and press F12 to open the developer tools on the Console tab.

You should see: a heading, a button, a sentence, two bullet points and a small form, with an address bar beginning file:///. Clicking anything does nothing at all yet, which is the point — a page has no behaviour until something listens.

If not: if the browser shows the HTML as plain text, the file was saved as .txt; rename it so .html is genuinely the last part of the name. Windows hides known extensions by default, so switch on File name extensions in Explorer’s View menu before deciding it is right.

2
Make the button do something

Go: open events.js in your editor.

Do: save these eight lines as the whole of events.js, reload the page with F5, and click the button three times.

const button = document.getElementById("clear");
const message = document.getElementById("message");
let clicks = 0;
button.addEventListener("click", () => {
  clicks = clicks + 1;
  message.textContent = `You have clicked ${clicks} time(s).`;
  console.log("click number", clicks);
});

You should see: the sentence changing to You have clicked 1 time(s)., then 2, then 3 — and the same numbers appearing in the console. Read the shape carefully: you handed addEventListener a function and did not call it. The browser keeps it and calls it later, once per click, for as long as the page is open.

If not: if the message jumps to its final state the instant the page loads and then never changes again, you wrote the handler with brackets after it. That calls your function immediately and hands the browser its result instead of the function itself. The rule is short: pass the name, never the call.

3
Look inside the event

Go: back to events.js.

Do: save these five lines as the whole of events.js, reload, and click the word milk.

document.getElementById("list").addEventListener("click", (event) => {
  console.log(event.type);
  console.log(event.target.tagName, event.target.textContent);
  console.log(event.currentTarget.id);
});

You should see: click, then LI milk, then list. Three things worth keeping. The handler receives an object describing what happened. event.target is what you actually clicked, while event.currentTarget is the element the listener is attached to — here the whole list, which never changes however many items you click. And tagName comes back in capitals, so a comparison against "li" is always false; write "LI".

If not: if nothing appears in the console, you clicked the gap beside the words rather than an item; click directly on the text. If event is undefined, the parameter is missing from the arrow function — the browser passes it, but only if you accept it.

4
One listener for a whole list — including items that do not exist yet

Go: back to events.js.

Do: save these nine lines as the whole of events.js, reload, then click each of the three items in turn.

const list = document.getElementById("list");
list.addEventListener("click", (event) => {
  if (event.target.tagName !== "LI") return;
  event.target.classList.toggle("done");
});
const extra = document.createElement("li");
extra.textContent = "coffee";
list.appendChild(extra);

You should see: three items now, and every one of them strikes through when clicked and un-strikes when clicked again. Including coffee, which was created after the listener was attached. That is the whole trick, and it is called event delegation: the listener sits on the parent and clicks travel up to it, so it catches items that did not exist when it was written. Attaching one listener per item would have missed coffee entirely.

If not: remove the tagName guard and click the blank space between items: the list itself becomes the target and the whole <ul> gets struck through. That guard is why delegation needs one — you are catching everything inside the parent, so you must say which parts you meant.

5
Stop the browser doing its own thing

Go: back to events.js one last time.

Do: save these twelve lines as the whole of events.js, reload, type coffee into the box and press Enter.

const list = document.getElementById("list");
const form = document.getElementById("add-form");
const input = document.getElementById("item");
form.addEventListener("submit", (event) => {
  event.preventDefault();
  if (input.value.trim() === "") return;
  const li = document.createElement("li");
  li.textContent = input.value.trim();
  list.appendChild(li);
  console.log("added", input.value.trim());
  input.value = "";
});

You should see: coffee appended to the list, the box emptied ready for the next one, added coffee in the console — and the page itself staying exactly where it was, console messages intact. Add two or three more and watch them accumulate.

If not: take event.preventDefault() out and the browser does what it has always done with a submitted form: it sends it and reloads the page. Your item flashes up and vanishes, the console clears, and it looks as though nothing happened. That single line is what turns a form from a page-load into part of an application. trim() earns its place too — without it a stray space counts as an item.

🎉
Check yourself before moving on

Without scrolling up: a to-do app attaches a click listener to every <li> when the page loads. New items added afterwards do nothing when clicked. Why, and what is the fix in one sentence? Answer: the loop only ever saw the items that existed at load time, and a listener cannot attach itself to an element that does not exist yet. Put one listener on the parent list instead and read event.target — delegation, as in step 4.

Now do it without the page: make each item removable. Add a second listener to the list that deletes an item when it is double-clicked. You have every piece already: the event name is dblclick, the guard is step 4’s tagName check, and the removal is event.target.remove().

Summary

  • Events are user actions (clicks, key presses, form submissions) that JavaScript can respond to
  • Use addEventListener("type", handler) to attach event handlers
  • The event object provides details about what happened (event.target, event.key, etc.)
  • Events propagate through the DOM: capturing (down) then bubbling (up)
  • Event delegation attaches one listener to a parent instead of many listeners to children
  • Use event.preventDefault() to stop default browser behaviors
  • Always use addEventListener instead of inline onclick attributes
🎉
Event handling mastered!

You can now make web pages respond to any user interaction. Next up: Async JavaScript — learn about Promises, async/await, and the Fetch API to work with servers and APIs.