~/hackweb.dev
JavaScript Event Delegation & Bubbling
Quiz
...

JavaScript Event Delegation & Bubbling

beginner · updated Tue Sep 08 2026Contribute

Master event bubbling, delegation, and efficient event handling.

JavaScript Event Delegation & Bubbling

Events bubble up from the target element to the root. This enables efficient event handling.

Event Bubbling

let parent = document.getElementById("parent");
let child = document.getElementById("child");

child.addEventListener("click", () => console.log("Child clicked"));
parent.addEventListener("click", () => console.log("Parent clicked"));

// Clicking child logs both — events bubble up

Stopping Propagation

child.addEventListener("click", (e) => {
  e.stopPropagation();  // Stops bubbling
  console.log("Child only");
});

Event Target vs Current Target

list.addEventListener("click", (e) => {
  console.log(e.target);        // The actual clicked element
  console.log(e.currentTarget); // The element with the listener
});

Remember: e.target = what was clicked. e.currentTarget = where the listener is.

Event Delegation

Add one listener to the parent instead of one per item:

// Bad: One listener per item
document.querySelectorAll(".item").forEach(item => {
  item.addEventListener("click", handler);
});

// Good: One listener on parent
document.getElementById("list").addEventListener("click", (e) => {
  if (e.target.classList.contains("item")) handler(e);
});

Why delegation matters: New items added later work automatically — no re-attaching listeners.

Using closest() and matches()

list.addEventListener("click", (e) => {
  if (e.target.matches(".item")) handler(e);
});

// Use closest() when clickable element has children
list.addEventListener("click", (e) => {
  let item = e.target.closest(".item");
  if (!item) return;
  handler(item);
});

Tip: closest() is more reliable when the target has nested child elements.

Complete Example: Todo List

let todoList = document.getElementById("todo-list");
let input = document.getElementById("todo-input");
let addBtn = document.getElementById("add-btn");

addBtn.addEventListener("click", () => {
  let text = input.value.trim();
  if (!text) return;
  let li = document.createElement("li");
  li.innerHTML = `<span class="text">${text}</span><button class="delete">Delete</button>`;
  todoList.appendChild(li);
  input.value = "";
});

todoList.addEventListener("click", (e) => {
  if (e.target.classList.contains("text")) {
    e.target.parentElement.classList.toggle("completed");
  }
  if (e.target.classList.contains("delete")) {
    e.target.parentElement.remove();
  }
});

Best Practices

  • Use delegation for dynamic content — New elements work automatically
  • Use closest() for parent matching — More reliable with nested elements
  • Use matches() for checking — Clean selector matching
  • Stop propagation when needed — Prevent unintended parent handlers
  • Use data attributes — Store extra info on elements

Common Mistakes

  • Not checking e.target — Handle all clicks unintentionally
  • Forgetting stopPropagation() — Unintended side effects
  • Using delegation everywhere — Simple cases don’t need it
  • Ignoring keyboard events — Accessibility matters
  • Not cleaning up listeners — Remove when no longer needed