LearnFrenzy
Sign in Take a free test

DOM, Events and Forms · Lesson 35 of 40

JavaScript Events and Event Delegation

5 min read Updated Nov 7, 2026 Saurabh Samir

An event is something that happens on the page: a click, a key press, a form submission, the page finishing loading. JavaScript reacts by attaching listeners – functions that run when the event fires. Understanding how events travel through the DOM leads to event delegation, a technique that keeps code fast and simple – and a classic interview question.

In this lesson

  • addEventListener and the event object
  • Bubbling and capturing
  • Event delegation
  • preventDefault, keyboard events, options and removing listeners

Listening for events

<button id="like-btn" type="button">👍 Like <span class="count">0</span></button>
const button = document.querySelector("#like-btn");
let likes = 0;

button.addEventListener("click", (event) => {
  likes++;
  button.querySelector(".count").textContent = likes;
  button.classList.toggle("liked", likes > 0);
  console.log(event.type, event.target.tagName, event.currentTarget.id);
});

Each click updates the count. Clicking exactly on the number logs click SPAN like-btn: event.target is the element actually clicked, while event.currentTarget is the element the listener is attached to.

Common eventsFires when
click, dblclickan element is clicked (also Enter/Space on buttons)
input, changea field's value changes (on every keystroke / when committed)
submita form is submitted
keydown, keyupa key is pressed / released
focus, bluran element gains / loses focus
pointerdown, pointermovemouse, pen or touch interaction
scroll, resizethe page scrolls / the window resizes
DOMContentLoaded, loadHTML parsed / everything (images too) loaded

Avoid inline handlers

<button onclick="like()"> mixes behaviour into HTML, allows only one handler, needs global functions and is blocked by strict Content Security Policies. Use addEventListener.

Bubbling and capturing

When you click an element, the event doesn't only fire there. It travels down from window to the target (capturing), fires on the target, then travels back up through every ancestor (bubbling). Listeners run during bubbling by default.

A click on a button inside li inside ul

  1. Capture phasewindow → document → … → ul → li
  2. Target phasethe button itself
  3. Bubble phaseli → ul → body → document → window
document.querySelector("ul").addEventListener("click", () => console.log("ul (bubble)"));
document.querySelector("ul").addEventListener("click", () => console.log("ul (capture)"), { capture: true });
document.querySelector("li").addEventListener("click", () => console.log("li"));
document.querySelector("button").addEventListener("click", () => console.log("button"));
// Clicking the button logs: ul (capture), button, li, ul (bubble)

event.stopPropagation() stops the event travelling further. Use it sparingly – other code (analytics, "click outside to close" menus) often relies on events bubbling.

Event delegation

Instead of attaching a listener to every item, attach one listener to their common parent and check event.target. It works for items added later, too:

<ul id="todo-list">
  <li data-id="1"><span>Revise closures</span> <button class="done">✓</button> <button class="delete">✕</button></li>
  <li data-id="2"><span>Practise SQL joins</span> <button class="done">✓</button> <button class="delete">✕</button></li>
</ul>
const list = document.querySelector("#todo-list");

list.addEventListener("click", (event) => {
  const button = event.target.closest("button");      // the clicked button, if any
  if (!button || !list.contains(button)) return;

  const item = button.closest("li");
  const id = item.dataset.id;

  if (button.matches(".delete")) {
    item.remove();
    console.log("Deleted", id);
  } else if (button.matches(".done")) {
    item.classList.toggle("completed");
    console.log("Toggled", id);
  }
});

Interview angle: why delegation?

  • Performance – one listener instead of hundreds.
  • Dynamic content – new items work without attaching new listeners.
  • Less memory and cleanup – nothing to remove when items are deleted.

It works because events bubble; closest() finds the relevant element even when the click lands on a child (an icon inside the button).

preventDefault

Some events have a built-in browser action: a link navigates, a form submits and reloads the page, a key types a character. event.preventDefault() cancels that action so your code can take over:

document.querySelector("#search-form").addEventListener("submit", async (event) => {
  event.preventDefault();                           // no page reload
  const query = new FormData(event.currentTarget).get("q").trim();
  if (query) await showResults(query);
});

document.querySelector(".external-warning").addEventListener("click", (event) => {
  if (!confirm("You are leaving LearnFrenzy. Continue?")) event.preventDefault();
});

Keyboard events

document.addEventListener("keydown", (event) => {
  // Ctrl+K or Cmd+K opens search
  if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
    event.preventDefault();
    document.querySelector("#search").focus();
  }
  if (event.key === "Escape") closeModal();
});

Use event.key ("Enter", "Escape", "ArrowUp", "a") – the older keyCode is deprecated.

Listener options and cleanup

// Runs only once, then removes itself
button.addEventListener("click", showWelcomeTour, { once: true });

// passive: promise not to call preventDefault – lets the browser scroll smoothly
window.addEventListener("scroll", updateProgressBar, { passive: true });

// Remove a listener: pass the SAME function reference
function onResize() { console.log(window.innerWidth); }
window.addEventListener("resize", onResize);
window.removeEventListener("resize", onResize);

// Remove many listeners at once with an AbortController
const controller = new AbortController();
window.addEventListener("resize", onResize, { signal: controller.signal });
document.addEventListener("keydown", onKey, { signal: controller.signal });
controller.abort();          // both removed – handy when a component is destroyed

Common mistake: removing an anonymous function

el.removeEventListener("click", () => {...}) does nothing – it's a new function, not the one you added. Keep a reference, or use { signal }.

Practice

Try it yourself

  1. Build a character counter: as the user types in a <textarea maxlength="200">, show "n/200" below it and add a warn class after 180.

    Show solution
    const area = document.querySelector("#bio");
    const counter = document.querySelector("#bio-count");
    area.addEventListener("input", () => {
      const n = area.value.length;
      counter.textContent = `${n}/200`;
      counter.classList.toggle("warn", n > 180);
    });

Quick check

event.target vs event.currentTarget?

target is the element where the event started; currentTarget is the element whose listener is running.

What is event delegation?

Handling events for many children with one listener on a parent, using bubbling and event.target.

How do you stop a form from reloading the page?

Call event.preventDefault() in the submit handler.

Summary

  • addEventListener attaches handlers; the event object describes what happened.
  • Events capture down and bubble up; delegation uses bubbling for one efficient listener.
  • preventDefault cancels browser actions; use once, passive and signal for clean listeners.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular