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
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);
}
});
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
addEventListenerinstead of inlineonclickattributes
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.