2 min read
On this page

Events

addEventListener

The only way you should attach event handlers in modern JavaScript.

const button = document.querySelector("#save-btn");
button.addEventListener("click", function(event) {
  console.log("Button clicked");
});

Never use inline handlers like <button onclick="handleClick()">. They mix behavior with markup and only allow one handler per event.

Removing Listeners

Pass the same function reference to remove a listener. Anonymous functions cannot be removed.

function handleClick(event) { console.log("Clicked"); }
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);

Options

button.addEventListener("click", handler, { once: true });      // run once
button.addEventListener("click", handler, { capture: true });    // capture phase
document.addEventListener("touchstart", handler, { passive: true }); // better scroll perf

The Event Object

Every handler receives an event object with details about what happened.

form.addEventListener("submit", function(event) {
  event.preventDefault();         // stop default browser behavior
  console.log(event.target);      // element that caused the event
  console.log(event.currentTarget); // element the listener is on
  console.log(event.type);        // "submit"
});

target vs currentTarget

event.target is the element the user interacted with. event.currentTarget is the element the listener is attached to. These differ when events bubble.

document.querySelector("#menu").addEventListener("click", function(event) {
  console.log(event.target.tagName);         // "LI" (what was clicked)
  console.log(event.currentTarget.tagName);  // "UL" (where listener is)
});

stopPropagation

Stops the event from traveling further. Use sparingly — it makes debugging harder.

document.querySelector(".dropdown-menu").addEventListener("click", function(event) {
  event.stopPropagation();  // clicks inside menu don't close it
});
document.addEventListener("click", () => closeAllDropdowns());

Event Bubbling & Capturing

Events go through three phases: capture (down from document), target, then bubble (back up). By default, listeners fire during the bubble phase.

document.querySelector("#outer").addEventListener("click", () => console.log("outer"));
document.querySelector("#inner").addEventListener("click", () => console.log("inner"));
document.querySelector("#btn").addEventListener("click", () => console.log("button"));
// Clicking #btn: "button" → "inner" → "outer"

Event Delegation

Attach one listener to the parent instead of one per child. More efficient, and it handles dynamically added elements.

<ul id="task-list">
  <li data-id="1">Task One <button class="delete">X</button></li>
  <li data-id="2">Task Two <button class="delete">X</button></li>
</ul>
document.querySelector("#task-list").addEventListener("click", function(event) {
  if (event.target.classList.contains("delete")) {
    const taskItem = event.target.closest("li");
    console.log(`Deleting task ${taskItem.dataset.id}`);
    taskItem.remove();
  }
});

closest() walks up the DOM to find the nearest ancestor matching a selector. It is essential for delegation because event.target might be a child of what you care about.

// Bad: one listener per item, breaks for new items
document.querySelectorAll(".item").forEach(item => {
  item.addEventListener("click", handleItemClick);
});

// Good: one listener handles current and future items
document.querySelector("#item-list").addEventListener("click", function(event) {
  const item = event.target.closest(".item");
  if (item) handleItemClick(item);
});

Common Events

Keyboard

document.addEventListener("keydown", function(event) {
  console.log(event.key);   // "Enter", "a", "Escape", "ArrowUp"
  if (event.ctrlKey && event.key === "s") {
    event.preventDefault();
    saveDocument();
  }
});

Form

form.addEventListener("submit", handler);   // form submitted
input.addEventListener("input", handler);   // every keystroke/change
input.addEventListener("change", handler);  // value changed on blur
input.addEventListener("focus", handler);   // gained focus
input.addEventListener("blur", handler);    // lost focus

Scroll & Resize

window.addEventListener("scroll", handler);  // fires frequently — throttle in production
window.addEventListener("resize", handler);  // also fires frequently

Page Lifecycle

document.addEventListener("DOMContentLoaded", () => {
  console.log("DOM ready");  // images/stylesheets may still be loading
});
window.addEventListener("load", () => {
  console.log("Everything loaded");
});

Use DOMContentLoaded for initializing JavaScript. Better yet, place your <script> at the end of <body> or use the defer attribute.

Custom Events

Create your own events with CustomEvent to decouple components.

// Dispatch a custom event with data
const productList = document.querySelector("#products");
productList.dispatchEvent(new CustomEvent("product:added", {
  detail: { id: 42, name: "Widget" },
  bubbles: true  // allows delegation
}));

// Listen for it anywhere up the tree
document.addEventListener("product:added", function(event) {
  console.log("New product:", event.detail.name);
  updateCartCount();
});

Custom events are useful when one part of the page needs to announce that something happened without knowing who is listening. The dispatching code and the listening code are completely independent.

Common Pitfalls

Forgetting preventDefault on form submit. The browser reloads the page by default.

Attaching listeners inside loops without delegation. Hundreds of individual listeners waste memory and miss dynamically added elements.

Overusing stopPropagation. It silently breaks delegation and analytics tracking upstream.

Not removing listeners when elements are removed. Listeners keep closures in memory. Use removeEventListener or { once: true }.

Using keyCode instead of key. event.keyCode is deprecated. Use event.key.

Key Takeaways

  • Always use addEventListener. Never inline handlers.
  • event.target is what was interacted with. event.currentTarget is where the listener lives.
  • Events bubble up from the target to the document. Use this for event delegation.
  • Event delegation means one listener on the parent, not many on the children. It handles dynamic elements automatically.
  • Use preventDefault to stop defaults. Use stopPropagation sparingly.
  • Custom events with CustomEvent decouple components without direct references.