Skip to content
Home
JavaScript DOM Events: A Complete Guide

JavaScript DOM Events: A Complete Guide

JavaScript JavaScript 8 min read 1492 words Beginner ExcellentWiki Editorial Team

DOM events are the mechanism that makes web pages interactive. When a user clicks a button, presses a key, submits a form, or resizes the browser window, the browser fires an event. Your JavaScript code listens for these events and responds to them. Understanding how events work — how they propagate through the DOM, how to attach and remove listeners, and how to structure event-driven code — is essential for building responsive web applications.

This guide covers everything you need to know about DOM events in JavaScript, from basic event listeners to advanced patterns like event delegation and custom events.

Adding Event Listeners

There are two ways to listen for events in the DOM: inline event handlers and the addEventListener method.

Inline Event Handlers

The oldest approach assigns a function directly to an event property on the element:

const button = document.getElementById("myButton");
button.onclick = function() {
  console.log("Button clicked!");
---;

This works but has significant limitations. You cannot attach multiple handlers to the same event on the same element — the second assignment overwrites the first. You also cannot control event propagation phases or remove handlers easily.

addEventListener

The modern approach uses addEventListener, which supports multiple handlers per event and provides fine-grained control:

const button = document.getElementById("myButton");

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

button.addEventListener("click", function(event) {
  console.log("This also runs!");
---);

Both handlers execute when the button is clicked. The third parameter accepts an options object or a boolean for capture phase control:

element.addEventListener("click", handler, {
  capture: false,    // true to run during capture phase
  once: true,        // auto-remove after first invocation
  passive: true      // hint that preventDefault won't be called (improves scroll perf)
---);

Common Event Types

Mouse Events

element.addEventListener("click", handler);     // Single click
element.addEventListener("dblclick", handler);  // Double click
element.addEventListener("mouseenter", handler); // Hover start (no bubble)
element.addEventListener("mouseleave", handler); // Hover end (no bubble)
element.addEventListener("mousemove", handler);  // Mouse movement

Keyboard Events

document.addEventListener("keydown", (event) => {
  console.log(`Key pressed: ${event.key}`);     // "Enter", "a", "Shift"
  console.log(`Key code: ${event.code}`);        // "Enter", "KeyA"
  console.log(`Is shift held: ${event.shiftKey}`);

  if (event.key === "Escape") {
    closeModal();
  }
---);

document.addEventListener("keyup", handler);    // Key released

Use keydown for most keyboard interactions. It fires repeatedly while a key is held and captures modifier keys (Shift, Ctrl, Alt, Meta) reliably.

Form Events

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

form.addEventListener("submit", (event) => {
  event.preventDefault();  // Stop page reload
  const data = new FormData(form);
  console.log(Object.fromEntries(data));
---);

input.addEventListener("focus", () => console.log("Input focused"));
input.addEventListener("blur", () => console.log("Input lost focus"));
input.addEventListener("input", () => console.log("Value changed"));
input.addEventListener("change", () => console.log("Value committed"));

The input event fires on every keystroke. The change event fires when the user commits the change (blur after modification).

Window Events

window.addEventListener("load", handler);       // Full page loaded
window.addEventListener("DOMContentLoaded", handler); // DOM ready, images may still load
window.addEventListener("resize", handler);     // Window resized
window.addEventListener("scroll", handler, { passive: true }); // Scroll
window.addEventListener("beforeunload", (e) => {
  e.preventDefault();
  e.returnValue = "";  // Shows confirmation dialog
---);

The Event Object

Every event handler receives an Event object with properties and methods that describe what happened:

document.addEventListener("click", (event) => {
  console.log(event.type);        // "click"
  console.log(event.target);      // The element that was actually clicked
  console.log(event.currentTarget); // The element the listener is attached to
  console.log(event.clientX, event.clientY); // Mouse position relative to viewport
  console.log(event.pageX, event.pageY);     // Mouse position relative to document
  console.log(event.timeStamp);   // Time since page load (ms)
  console.log(event.bubbles);     // Whether the event bubbles (true for click)
---);

Preventing Default Behavior

link.addEventListener("click", (event) => {
  event.preventDefault();  // Stop navigation
  // Handle the click ourselves
---);

Stopping Propagation

child.addEventListener("click", (event) => {
  event.stopPropagation();           // Stop bubbling
  // or
  event.stopImmediatePropagation();  // Stop bubbling AND other handlers on this element
---);

Event Propagation: Bubbling and Capturing

When an event fires on a DOM element, it travels through the DOM tree in three phases:

  1. Capture phase — The event travels from document down to the target element
  2. Target phase — The event reaches the target element
  3. Bubble phase — The event travels from the target back up to document

Most event listeners run during the bubble phase (the default). To listen during the capture phase, pass true as the third argument:

// Capture phase listener (runs before target's handlers)
document.addEventListener("click", () => console.log("Capture"), true);

// Bubble phase listener (runs after target's handlers)
document.addEventListener("click", () => console.log("Bubble"), false);

Clicking a nested element demonstrates the full path:

<div id="outer">
  <div id="inner">
    <button id="btn">Click</button>
  </div>
</div>
document.getElementById("outer").addEventListener("click", () => console.log("outer"));
document.getElementById("inner").addEventListener("click", () => console.log("inner"));
document.getElementById("btn").addEventListener("click", () => console.log("btn"));

// Clicking the button logs: "btn" → "inner" → "outer" (bubble phase)

Event Delegation

Event delegation leverages bubbling to handle events on many elements with a single listener. Instead of attaching a handler to each child, attach one to a parent and use event.target to determine which child was interacted with:

// Instead of this:
document.querySelectorAll("li").forEach(li => {
  li.addEventListener("click", () => li.classList.toggle("done"));
---);

// Do this — one listener for any number of list items:
document.querySelector("ul").addEventListener("click", (event) => {
  if (event.target.tagName === "LI") {
    event.target.classList.toggle("done");
  }
---);

Event delegation is especially useful for dynamically created elements:

const list = document.querySelector("ul");

list.addEventListener("click", (event) => {
  if (event.target.dataset.action === "delete") {
    event.target.closest("li").remove();
  }
---);

// Later, add new items — they automatically get click handling
function addItem(text) {
  const li = document.createElement("li");
  li.innerHTML = `${text} <button data-action="delete">✕</button>`;
  list.appendChild(li);
---

Delegation with Data Attributes

Using data-* attributes makes delegation cleaner and more scalable:

toolbar.addEventListener("click", (event) => {
  const action = event.target.closest("[data-action]")?.dataset.action;
  if (!action) return;

  switch (action) {
    case "bold": document.execCommand("bold"); break;
    case "italic": document.execCommand("italic"); break;
    case "save": saveDocument(); break;
  }
---);

Removing Event Listeners

Always remove event listeners when they are no longer needed, especially in SPAs where components mount and unmount. Use removeEventListener with the same function reference:

function handleClick(event) {
  console.log("Clicked");
---

element.addEventListener("click", handleClick);

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

Anonymous functions cannot be removed because there is no reference:

// Cannot be removed
element.addEventListener("click", () => console.log("Clicked"));

AbortController for Multiple Cleanups

Modern browsers support AbortSignal to remove multiple listeners at once:

const controller = new AbortController();
const { signal } = controller;

window.addEventListener("resize", handleResize, { signal });
window.addEventListener("scroll", handleScroll, { signal });
document.addEventListener("keydown", handleKeydown, { signal });

// Remove all three at once
controller.abort();

Custom Events

You can create and dispatch your own events:

// Create a custom event with data
const event = new CustomEvent("userLogin", {
  detail: { userId: 42, username: "alice" }
---);

// Dispatch it on any element
document.dispatchEvent(event);

// Listen for it elsewhere
document.addEventListener("userLogin", (event) => {
  console.log(`User logged in: ${event.detail.username}`);
---);

Custom events keep your code decoupled. One part of your application dispatches the event; other parts listen without needing a direct reference to each other.

Best Practices

Use addEventListener over inline handlers — inline handlers (onclick="...") mix markup with behavior and limit you to one handler per event. addEventListener is cleaner and more flexible.

Use event delegation for dynamic content — attaching a single listener to a parent is more efficient than attaching listeners to every child, especially when children are added and removed dynamically.

Remove listeners when they are no longer needed — leaked listeners keep references to DOM elements and prevent garbage collection, causing memory leaks in long-lived applications.

Use passive listeners for scroll and touch events{ passive: true } tells the browser your handler will not call preventDefault(), allowing the browser to optimize scrolling performance.

Prefer event.currentTarget over event.target when checking the listener elementtarget is the element that initiated the event, while currentTarget is the element the listener is attached to. They differ when using delegation.

Summary

DOM events are the bridge between user interaction and JavaScript behavior. Mastering addEventListener, understanding the capture and bubble phases, using event delegation to handle dynamic content efficiently, and cleaning up listeners properly will make your web applications more responsive, performant, and maintainable.

FAQ

What is the difference between var, let, and const?

var is function-scoped and hoisted. let and const are block-scoped. const cannot be reassigned (but objects can mutate). Prefer const by default, use let when reassignment is needed, and avoid var in modern code.

How does the JavaScript event loop work?

The event loop continuously checks the call stack and task queues. Macrotasks (setTimeout, I/O) are processed one per loop iteration. All microtasks (Promise callbacks, queueMicrotask) are processed between macrotasks. UI rendering occurs after microtask processing.

What is the difference between == and ===?

== performs type coercion before comparison (1 == ’1’ is true). === requires both value and type to be identical (1 === ’1’ is false). Always use === unless you explicitly need type coercion.

When should I use promises vs async/await?

Async/await is syntactic sugar over promises, making asynchronous code read synchronously. Prefer async/await for readability. Use promise combinators (Promise.all, Promise.race) when managing multiple concurrent operations.

What are the most common JavaScript memory leaks?

Global variables, forgotten timers/intervals, detached DOM nodes, closures retaining large objects, and event listeners not removed. Use Chrome DevTools Memory panel to identify leaks. WeakMap/WeakSet help with cache-related leaks.

For a comprehensive overview, read our article on Javascript Arrays Guide.

For a comprehensive overview, read our article on Javascript Async Await.

Section: JavaScript 1492 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top