Last 30 Days
No notifications
Events are signals fired by the browser when something happens — a click, a keypress, a scroll, a form submission. Event listeners let your JavaScript respond to these signals.
element.addEventListener("click", handler);
element.removeEventListener("click", handler);Always use addEventListener over inline onclick — it supports multiple listeners and options.
| Category | Events | |||
| Mouse | click, dblclick, mouseenter, mouseleave | |||
| Keyboard | keydown, keyup, keypress (deprecated) | |||
| Form | submit, input, change, focus, blur | |||
| Window | load, resize, scroll, DOMContentLoaded | |||
| Touch | touchstart, touchmove, touchend | The Event ObjectEvery handler receives an event object with useful properties: | Property | Description |
e.target | The element that originated the event | |||
e.currentTarget | The element the listener is attached to | |||
e.type | Event type ("click", "keydown", etc.) | |||
e.preventDefault() | Stop default behavior (e.g., form submit) | |||
e.stopPropagation() | Stop event from bubbling up |
Events bubble up from the target to the root:
Click on <button> inside <div> inside <body>:button → div → body → html → document
Instead of adding listeners to every child, add ONE listener to the parent and check e.target:
list.addEventListener("click", (e) => {
if (e.target.matches("li")) {
handleItemClick(e.target);
}
});This is efficient, works with dynamically added elements, and uses less memory.
The web is reactive. The user clicks, types, scrolls, resizes, drags a file in — each of those is an event that the browser fires. Your job in JavaScript is simply: "when *X* happens, run *this* function."
That function is called an event handler (or *listener*).
addEventListenerconst button = document.querySelector("#save");button.addEventListener("click", () => {
console.log("Saved!");
});
Three pieces:
1. The element you're listening on (button).
2. The event name as a string ("click").
3. The handler — a function that runs each time it fires.
That's the entire mental model. Everything else is just more event names and details.
| Event | Fires when |
click | The element is clicked |
input | A form field's value changes (every keystroke) |
change | A field loses focus after value changed |
submit | A is submitted |
keydown / keyup | A key is pressed / released |
mouseenter / mouseleave | Mouse enters/leaves the element |
scroll | Element (or window) is scrolled |
load | Image / page finished loading |
DOMContentLoaded | HTML is parsed (don't wait for images) |
Your handler receives one argument — a rich event object describing what happened:
button.addEventListener("click", (e) => {
console.log(e.target); // the element clicked
console.log(e.currentTarget); // the element the listener is on
console.log(e.type); // "click"
e.preventDefault(); // cancel default behaviour
});For specific events you get extras:
e.clientX, e.clientY, e.buttone.key ("Enter"), e.code ("Enter"), e.shiftKeye.target.valueIf you want to stop listening, you must pass the same function reference you used to add it:
function onClick() { /* … */ }
button.addEventListener("click", onClick);
button.removeEventListener("click", onClick);This is why anonymous arrow functions can't be removed individually.
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
e.preventDefault(); // stop the default page reload
const data = new FormData(form);
console.log(Object.fromEntries(data));
});FormData + preventDefault is the classic combo for grabbing form values.
1. Forgetting e.preventDefault() on a — the page reloads and your data vanishes.
2. Re-binding listeners every render — leaks memory in long-lived apps. Add once, remove on cleanup.
3. Using onclick="…" HTML attributes — works but mixes concerns. Prefer addEventListener.
4. Listening on every list item instead of using *delegation* (next section).
5. Trying to remove an arrow function by re-defining it — that's a *new* reference.
When you click a button inside a card inside a section, the browser doesn't only fire on the button. The event walks the DOM in three phases:
1. Capturing → document ⤵ html ⤵ body ⤵ section ⤵ card ⤵ BUTTON
2. Target → BUTTON (the click hits)
3. Bubbling → button ⤴ card ⤴ section ⤴ body ⤴ html ⤴ documentBy default, listeners fire on the bubbling phase. To listen on the way down instead:
parent.addEventListener("click", handler, { capture: true });e.stopPropagation(); // don't bubble further
e.stopImmediatePropagation(); // also skip other listeners on this element
e.preventDefault(); // cancel the default browser actionImagine a list of 500 todo items, each with a delete button. Adding 500 listeners is wasteful and breaks when items are added later. Instead, listen on the parent and use event.target to find what was actually clicked:
list.addEventListener("click", (e) => {
const deleteBtn = e.target.closest(".delete");
if (!deleteBtn) return;
const id = deleteBtn.dataset.id;
removeTodo(id);
});element.closest(selector) walks up from the click target until it finds a match — robust even when buttons contain nested icons.
Delegation shines for:
You can invent your own events for clean cross-component communication:
const event = new CustomEvent("cart:update", {
detail: { itemId: 42, quantity: 2 },
bubbles: true,
});
cartButton.dispatchEvent(event);document.addEventListener("cart:update", (e) => {
console.log(e.detail.itemId);
});
The cart button doesn't need to know who's listening, and the listener doesn't need to know who fired it. Loose coupling for free.
Touch and scroll listeners can block scrolling because the browser has to wait to see if you'll call preventDefault(). Tell it you won't:
window.addEventListener("scroll", onScroll, { passive: true });This single flag dramatically improves scroll smoothness on mobile.
High-frequency events (scroll, resize, mousemove, input) can fire 60+ times per second. You almost never want to react that often.
function debounce(fn, wait = 300) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}searchInput.addEventListener("input", debounce((e) => {
fetchResults(e.target.value);
}, 250));
AbortController for Bulk CleanupModern way to remove many listeners at once — perfect for component lifecycle:
const ctrl = new AbortController();window.addEventListener("scroll", onScroll, { signal: ctrl.signal });
window.addEventListener("resize", onResize, { signal: ctrl.signal });
button.addEventListener("click", onClick, { signal: ctrl.signal });
// Later — remove ALL three at once:
ctrl.abort();
Cleaner than tracking each handler reference individually.
Instead of writing separate mouse and touch handlers, use pointerdown, pointermove, pointerup. They expose e.pointerType ("mouse" / "touch" / "pen") and unify the code path.
When dragging, the cursor often leaves the element and the drag breaks. Lock pointer events to the element:
handle.addEventListener("pointerdown", (e) => {
handle.setPointerCapture(e.pointerId);
});All subsequent move/up events for that pointer go to handle, even outside its bounds.
1. Build a button counter — every click increments a number on screen.
2. Build a live search box that calls fetch only after the user has stopped typing for 300 ms (debounce).
3. Replace 50 individual listeners with one delegated listener on the .
4. Add a global keyboard shortcut (e.g. Ctrl + K opens a modal) using document.addEventListener("keydown", …).