JavaScript Event Listeners: Click, Submit, Scroll and Delegation

An event listener is a function the browser calls when something happens to an element, such as a click, a form submit or a scroll. You attach one with element.addEventListener(type, handler, options). The options object turns on once, passive, capture or an abort signal, and removeEventListener detaches the same function again later.

addEventListener options
OptionDefaultEffect
oncefalseRemoves the listener automatically after its first call
passivefalse (true for wheel and touch on window, document and body)Promises not to call preventDefault, so scrolling never waits
capturefalseRuns on the way down to the target instead of on the way back up
signalnoneAn AbortSignal; aborting it removes the listener

Updated

Events are easier to understand when you can see them. The event log below records every listener call on a small test area, so you can click, type, submit and scroll and watch the order, the target and the effect of each option. Then copy the patterns for the jobs you actually have.

Live event log

Click, type, submit and scroll in the test area. Every listener call is written to the log with its type, target and current target.

Event types you will use most

Common events and where to listen for them
EventFires whenBubbles?
clickA pointer press and release, or Enter or Space on a buttonYes
inputThe value of a field changes as the visitor typesYes
changeA field's value is committed, for example on blur or selectionYes
submitA form is submitted; fires on the form elementYes
keydownA key is pressedYes
focus / blurAn element gains or loses focusNo
focusin / focusoutSame as focus and blur, but bubblingYes
DOMContentLoadedThe HTML is parsed and deferred scripts have run; listen on documentNot relevant

Copyable patterns

Click with a named handler you can remove

Keeping the function in a variable is what makes removeEventListener work.

const button = document.querySelector("#menu-button");

function toggleMenu() {
  const open = button.getAttribute("aria-expanded") === "true";
  button.setAttribute("aria-expanded", String(!open));
}

button.addEventListener("click", toggleMenu);
// later
button.removeEventListener("click", toggleMenu);

Handle a form submit without a reload

preventDefault stops the browser's own navigation. event.submitter tells you which submit button was used.

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

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const data = new FormData(form);
  const status = form.querySelector("[role=status]");
  status.textContent = "Sending...";
  try {
    const res = await fetch(form.action, { method: "POST", body: data });
    status.textContent = res.ok ? "Thanks, you are on the list." : "Something went wrong.";
  } catch {
    status.textContent = "Network error, please try again.";
  }
});

Event delegation on a list

One listener on the list handles every delete button, including rows added after the page loaded.

const list = document.querySelector("#todo-list");

list.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-action=delete]");
  if (!button || !list.contains(button)) return;
  button.closest("li").remove();
});

A passive scroll listener throttled to one update per frame

passive: true lets scrolling start right away, and requestAnimationFrame keeps the work to once per frame.

const header = document.querySelector("header");
let queued = false;

window.addEventListener("scroll", () => {
  if (queued) return;
  queued = true;
  requestAnimationFrame(() => {
    header.classList.toggle("is-scrolled", window.scrollY > 40);
    queued = false;
  });
}, { passive: true });

once and AbortController

The first listener runs a single time. The others are all removed together by one abort() call.

document.querySelector("#cookie-ok")
  .addEventListener("click", () => banner.remove(), { once: true });

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

window.addEventListener("resize", onResize, { signal });
document.addEventListener("keydown", onKey, { signal });

// when this feature is torn down:
controller.abort();

Keyboard shortcut that ignores typing in fields

Checking the target stops the shortcut from firing while someone is typing a slash into a search box.

document.addEventListener("keydown", (event) => {
  const typing = event.target.closest("input, textarea, select, [contenteditable]");
  if (event.key === "/" && !typing) {
    event.preventDefault();
    document.querySelector("#search").focus();
  }
});

How an event travels

A dispatched event moves in three phases. It travels down from the window to the target in the capture phase, reaches the target, then bubbles back up through each ancestor. Listeners run in the bubble phase unless you ask for capture.

stopPropagation halts the journey, so ancestors never hear it. Use it rarely, since delegated listeners and analytics further up the tree silently stop working.

Scroll without slowing the page

Scroll events can fire many times per frame of movement. Keep the handler tiny, read positions instead of writing layout, and batch visual updates inside requestAnimationFrame.

For jobs like revealing elements as they enter view, an IntersectionObserver is usually cheaper than measuring on every scroll. The scrollend event, Baseline since December 2025 according to MDN, fires once when movement stops.

Cleaning up listeners

Listeners on long-lived objects such as window or document keep their handler, and anything it references, alive. When the part of the page that needed them goes away, remove them.

The signal option makes this tidy. Create one AbortController per feature, pass its signal to every listener, and call abort() once during teardown.

How it works

  1. Use the controls in the test area: click the buttons, type in the field, submit the form and scroll the box.
  2. Each listener call appears in the log with its event type, the element that started it and the element that handled it.
  3. Turn on once to see a listener disappear after one call, or turn off delegation to see the list handle clicks per item instead.
  4. Copy any of the examples below. Every one is plain JavaScript with no library.

Tips

Frequently asked questions

What is event delegation?

Instead of attaching a listener to every item, you attach one to a shared parent. Because most events bubble up, the parent hears clicks from all its children, including items added later, and uses event.target to tell which one was clicked.

Why does removeEventListener not remove my listener?

It must receive the same function reference and the same capture setting that addEventListener was given. Two arrow functions with identical code are still different functions, so define the handler once and pass that variable both times.

What does passive: true do?

It tells the browser your handler will not call preventDefault, so the page can start scrolling immediately rather than waiting for your code to finish. Browsers already default to passive for wheel and touch listeners on window, document and body.

What is the difference between target and currentTarget?

target is the innermost element where the event started, and it stays the same as the event bubbles. currentTarget is the element whose listener is running right now, which changes at each level.

Does form.submit() fire the submit event?

No. Calling form.submit() from a script skips the submit event and skips validation. Call form.requestSubmit() when you want your submit listeners and the built-in checks to run.

addEventListener or onclick?

The onclick property holds one function, so assigning a second one replaces the first. addEventListener lets several independent listeners share an element and supports the options object, so it is the better default.

Related: JavaScript accordion and tabs, JavaScript fetch and async, How to add a contact form, localStorage in JavaScript, Keyboard and screen reader testing.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap