Skip to main content
๐ŸŒbeginner

Events & Delegation

Browser events let you respond to user actions โ€” clicks, keypresses, form submissions. Learn event listeners, the event object, bubbling, and the efficient event delegation pattern.

Adding Event Listeners

hljs javascript
const button = document.querySelector("#myBtn");

// addEventListener is the modern, preferred approach
button.addEventListener("click", function(event) {
  console.log("Clicked!", event);
});

// Arrow function
button.addEventListener("click", (e) => {
  console.log("Button clicked at:", e.clientX, e.clientY);
});

// Named function (easier to remove later)
function handleClick(e) {
  console.log("Clicked");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // remove exact same reference

โš ๏ธAvoid inline handlers and onclick property

element.onclick = fn overwrites any previous handler. addEventListener stacks handlers safely and gives you more control (capture phase, once, etc.).

The Event Object

The event handler receives an Event object with information about what happened:

hljs javascript
button.addEventListener("click", (e) => {
  e.type;          // "click"
  e.target;        // the element that was clicked
  e.currentTarget; // the element the listener is attached to
  e.timeStamp;     // when the event occurred

  // Mouse events
  e.clientX;  e.clientY;  // position relative to viewport
  e.pageX;    e.pageY;    // position relative to document
  e.button;               // 0=left, 1=middle, 2=right

  // Keyboard events
  e.key;        // "Enter", "a", "ArrowLeft"
  e.code;       // "KeyA", "Space", "Enter" (physical key)
  e.ctrlKey;    // true if Ctrl held
  e.metaKey;    // true if Cmd (Mac) / Win key held
  e.shiftKey;   e.altKey;

  // Prevent default behavior
  e.preventDefault(); // stop browser's default action (e.g., form submit, link follow)

  // Stop propagation
  e.stopPropagation(); // stop event from bubbling further
});

Common Events

hljs javascript
// Mouse
element.addEventListener("click", handler);
element.addEventListener("dblclick", handler);
element.addEventListener("mouseenter", handler); // hover start (no bubbling)
element.addEventListener("mouseleave", handler); // hover end (no bubbling)
element.addEventListener("mousemove", handler);
element.addEventListener("mousedown", handler);
element.addEventListener("mouseup", handler);
element.addEventListener("contextmenu", handler); // right-click

// Keyboard
document.addEventListener("keydown", handler);
document.addEventListener("keyup", handler);
document.addEventListener("keypress", handler); // deprecated, use keydown

// Form
form.addEventListener("submit", (e) => {
  e.preventDefault(); // stop form from navigating/reloading
  const data = new FormData(form);
});
input.addEventListener("change", handler);  // fires when value changes and loses focus
input.addEventListener("input", handler);   // fires on every keystroke
input.addEventListener("focus", handler);
input.addEventListener("blur", handler);

// Window/Document
window.addEventListener("load", handler);       // all resources loaded
document.addEventListener("DOMContentLoaded", handler); // HTML parsed (before images)
window.addEventListener("resize", handler);
window.addEventListener("scroll", handler);

Event Bubbling and Capturing

Events travel through the DOM in two phases:

  1. Capture phase โ€” from document down to the target
  2. Bubble phase โ€” from target back up to document
hljs html
<div id="outer">
  <div id="middle">
    <button id="inner">Click</button>
  </div>
</div>
hljs javascript
// By default, listeners use the BUBBLE phase
document.getElementById("outer").addEventListener("click", () => {
  console.log("outer (bubble)");  // fires 3rd
});
document.getElementById("middle").addEventListener("click", () => {
  console.log("middle (bubble)"); // fires 2nd
});
document.getElementById("inner").addEventListener("click", () => {
  console.log("inner (bubble)");  // fires 1st (target)
});

// Click inner: "inner" โ†’ "middle" โ†’ "outer"

// Use capture phase with {capture: true}
document.getElementById("outer").addEventListener("click", () => {
  console.log("outer (capture)"); // fires 1st (before target)
}, { capture: true });

stopPropagation and preventDefault

hljs javascript
// Stop bubbling
inner.addEventListener("click", (e) => {
  e.stopPropagation(); // "outer" and "middle" handlers won't fire
});

// Prevent default browser behavior
const link = document.querySelector("a");
link.addEventListener("click", (e) => {
  e.preventDefault(); // stop navigation
  console.log("Link clicked but not followed");
});

const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
  e.preventDefault(); // stop page reload
  // handle form with JS instead
});

Event Delegation

Instead of attaching listeners to each child, attach ONE listener to the parent. Use event.target to identify which child was clicked:

hljs javascript
// โœ— Inefficient โ€” one listener per item
document.querySelectorAll(".list-item").forEach(item => {
  item.addEventListener("click", handler);
});

// โœ“ Event delegation โ€” one listener on the parent
const list = document.querySelector(".list");

list.addEventListener("click", (e) => {
  // Check if the clicked element matches what we want
  const item = e.target.closest(".list-item");
  if (!item) return; // clicked outside a list item

  console.log("Clicked item:", item.dataset.id, item.textContent);
});

Benefits of delegation:

  • Works for dynamically added elements (no need to re-attach)
  • Fewer event listeners = better memory/performance
  • Single place to manage list behavior

once, passive, and signal Options

hljs javascript
// {once: true} โ€” auto-removes after first call
button.addEventListener("click", handler, { once: true });

// {passive: true} โ€” promises not to call preventDefault
// Used for scroll/touch handlers to improve performance
window.addEventListener("scroll", handler, { passive: true });
window.addEventListener("touchmove", handler, { passive: true });

// AbortController โ€” remove multiple listeners at once
const controller = new AbortController();
const { signal } = controller;

document.addEventListener("click", handler, { signal });
document.addEventListener("keydown", handler, { signal });

// Remove all listeners
controller.abort(); // both handlers removed!

Custom Events

hljs javascript
// Create and dispatch custom events
const event = new CustomEvent("user-login", {
  detail: { userId: 42, name: "Alice" }, // custom data
  bubbles: true,
  cancelable: true,
});

document.dispatchEvent(event);

// Listen for it
document.addEventListener("user-login", (e) => {
  console.log("User logged in:", e.detail.name);
});

Throttle and Debounce

High-frequency events (scroll, resize, input) need rate limiting:

hljs javascript
// Debounce โ€” fires only after activity stops for 'wait' ms
function debounce(fn, wait) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}

// Good for: search input, window resize
const handleSearch = debounce((e) => {
  fetchResults(e.target.value);
}, 300);
input.addEventListener("input", handleSearch);

// Throttle โ€” fires at most once per 'limit' ms
function throttle(fn, limit) {
  let lastRan;
  return function(...args) {
    if (!lastRan || Date.now() - lastRan >= limit) {
      lastRan = Date.now();
      fn.apply(this, args);
    }
  };
}

// Good for: scroll, mousemove
const handleScroll = throttle(() => {
  updateScrollIndicator();
}, 100);
window.addEventListener("scroll", handleScroll);
โ–ถTry it yourself

Key Takeaways

  • addEventListener is the correct way to attach event handlers
  • The event object contains info about what happened (target, position, keys, etc.)
  • e.preventDefault() stops browser defaults; e.stopPropagation() stops bubbling
  • Events bubble from target โ†’ ancestors (capture goes the other direction)
  • Event delegation: one parent listener handles all child events โ€” works for dynamic elements
  • { once: true } auto-removes; { passive: true } signals no preventDefault for scroll events
  • Debounce and throttle prevent handlers from firing too frequently

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’