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();
    }
});

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.