Events

addEventListener, bubbling and capturing, delegation for dynamic lists, and forms without page reloads.

Listening

btn.addEventListener('click', event => {
  console.log(event.target);   // what was clicked
  console.log(event.currentTarget); // where the listener lives
});

// remove requires the same function reference
function handler() {}
el.addEventListener('click', handler);
el.removeEventListener('click', handler);
Common eventFires when
clickMouse click or keyboard activation
inputValue changes as the user types
changeValue committed (blur for text)
submitForm submitted
keydownKey pressed
DOMContentLoadedHTML parsed

Bubbling and capturing

An event travels three phases: capture down from the root, the target, then bubble back up. Listeners default to the bubble phase.

el.addEventListener('click', fn);              // bubble (default)
el.addEventListener('click', fn, true);        // capture
el.addEventListener('click', fn, { once: true, passive: true });

event.stopPropagation();  // stop further travel
event.preventDefault();   // stop the default action
⚠️
stopPropagation breaks analytics and delegated handlers globally. Prefer checking event.target in a delegate handler instead.

Event delegation

Instead of attaching a listener to every row, attach one to a stable parent and ask what was clicked. This works for elements added later and uses far less memory.

list.addEventListener('click', e => {
  const btn = e.target.closest('[data-action]');
  if (!btn) return;              // click was elsewhere
  handle(btn.dataset.action, btn.dataset.id);
});

Forms without reload

form.addEventListener('submit', async e => {
  e.preventDefault();                       // stop navigation
  const data = new FormData(form);
  const payload = Object.fromEntries(data);
  await fetch(form.action, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  });
});
💡
Bind to the form's submit, not the button's click — you keep Enter-key behaviour and validation for free.

FAQ

Why does my handler run twice?
Usually the listener was added twice (a re-render without cleanup), or the event bubbles into another listener. Check both.
target vs currentTarget?
target is the deepest element clicked; currentTarget is the element whose listener is executing. With delegation they routinely differ.

Working with the DOM Async JavaScript and fetch

Last refreshed 2026-09-17.