JavaScript Events
Handle browser events, event delegation, bubbling, capturing, and preventDefault with vanilla JavaScript.
TL;DR
- 01Attach event listeners with addEventListener for flexible handling.
- 02Use event delegation to handle many items with one listener.
- 03Control event flow with stopPropagation and preventDefault when needed.
Tips
- 01Use event delegation to attach a single listener to a container instead of many listeners on child elements.
- 02Pass the { once: true } option to addEventListener when a handler should only run a single time.
- 03Check event.cancelable before calling preventDefault, since some events like scroll cannot be canceled at all.
Warnings
- 01preventDefault only works on cancelable events — always check if the event is cancelable before calling it.
- 02Inline handlers like onclick can only hold one function, so a second assignment silently replaces the first one.
- 03Forgetting removeEventListener on elements you remove from the DOM can leak memory in long-running single-page applications.
Basic Event Listeners
addEventListener()Attach event handlers for flexible, composable event handling.
const button = document.querySelector("button");
button.addEventListener("click", (event) => {
console.log("Button clicked");
});The event objectContains details about what happened, like type, target, and position.
button.addEventListener("click", (e) => {
console.log(e.type); // "click"
console.log(e.target); // the button element
console.log(e.clientX); // mouse position
});removeEventListener()Removes a listener when it's no longer needed.
const handler = (e) => console.log("clicked");
button.addEventListener("click", handler);
button.removeEventListener("click", handler);Avoid inline handlersInline event handlers like onclick are outdated compared to addEventListener.
// Avoid: <button onclick="handleClick()">Click</button>
// Use addEventListener instead{ once: true }Fires a listener only one time, then removes it automatically.
button.addEventListener("click", handler, { once: true });
// Handler is automatically removed after first clickEvent Bubbling and Capturing
Bubbling by defaultEvents bubble up from child to parent by default.
div.addEventListener("click", () => console.log("div clicked"));
button.addEventListener("click", () => console.log("button clicked"));
// Clicking button logs both "button clicked" and "div clicked"stopPropagation()Prevents an event from bubbling up to parent listeners.
button.addEventListener("click", (e) => {
e.stopPropagation();
console.log("button only");
});Capture phasePass true as the third argument to listen during the capture phase.
div.addEventListener("click", handler, true);
// Capture phase runs before bubble phaseMost events bubbleMost events bubble, but check the MDN docs for specific events that don't.
Blocking parent handlersUse stopPropagation to prevent parent handlers from running at all.
Preventing Default Behavior
preventDefault()Stops the browser's default action for an event.
form.addEventListener("submit", (e) => {
e.preventDefault();
// Form does not submit to server
console.log("Form submission intercepted");
});Common use casesWorks on clickable links, form submissions, and other cancelable events.
link.addEventListener("click", (e) => {
e.preventDefault();
// Link does not navigate to href
});event.defaultPreventedCheck whether preventDefault was already called on the event.
if (!event.defaultPrevented) {
// Default action will occur
}Not all events cancelableNot all events can be prevented — check if the event is cancelable first.
event.cancelableVerify the event supports preventDefault before calling it.
link.addEventListener("click", (e) => {
if (e.cancelable) {
e.preventDefault();
}
});Event Delegation
One listener, many childrenAttach a single listener to a parent to handle children efficiently.
const list = document.querySelector("ul");
list.addEventListener("click", (e) => {
if (e.target.tagName === "LI") {
console.log("Clicked item:", e.target.textContent);
}
});Fewer listenersThis pattern is efficient when there are many similar elements.
// Instead of attaching listeners to each item:
items.forEach(item => item.addEventListener("click", handler));
// Attach one listener to the container:
container.addEventListener("click", handler);e.target.closest()Finds the closest matching ancestor from the actual clicked element.
document.addEventListener("click", (e) => {
const button = e.target.closest("button");
if (button) console.log("Button clicked");
});Works with dynamic elementsDelegation works with elements added to the DOM after the listener was attached.
Common Events
Mouse eventsclick, dblclick, mousedown, mouseup, and mousemove track pointer activity.
element.addEventListener("mousemove", (e) => {
console.log(`Mouse at ${e.clientX}, ${e.clientY}`);
});Keyboard eventskeydown and keyup track key presses; keypress is deprecated.
document.addEventListener("keydown", (e) => {
console.log(`Key pressed: ${e.key}`);
});Form eventschange, input, submit, reset, focus, and blur track form interaction.
input.addEventListener("input", (e) => {
console.log(`Current value: ${e.target.value}`);
});Window eventsload, unload, scroll, and resize track the page and viewport.
window.addEventListener("scroll", () => {
console.log("Page scrolled");
});In Practice
One delegated submit listener validates required fields and prevents submission until every field passes.
- 01The submit listener checks every required field before the browser submits the form.
- 02e.preventDefault() only runs when validation fails, so a valid form submits normally.
- 03A single delegated input listener clears the error class as the user types, without per-field listeners.
- 04closest('[required]') confirms the input that fired the event is actually one that needs validation.
const form = document.querySelector('#signup-form');
form.addEventListener('submit', (e) => {
const invalid = [...form.querySelectorAll('[required]')].filter(field => !field.value.trim());
if (invalid.length > 0) {
e.preventDefault();
invalid.forEach(field => field.classList.add('error'));
console.log(`${invalid.length} field(s) missing`);
}
});
form.addEventListener('input', (e) => {
const field = e.target.closest('[required]');
if (field) field.classList.remove('error');
});FAQ
Bubbling propagates events from the target element up to the root; capturing goes from root down to the target. Pass true as the third argument to addEventListener to use capturing phase instead of the default bubbling.
Call event.stopPropagation() inside your handler to prevent the event from bubbling up. Use event.stopImmediatePropagation() if you also want to block other listeners on the same element.
Attach one listener to a parent element and use event.target to identify which child triggered the event. This is especially useful for dynamically added elements or large lists. Listeners on the parent automatically cover new children.
Not all events are cancelable — for example, scroll events cannot be prevented after they fire. Check event.cancelable before calling preventDefault(), and for scroll/touch performance consider using a passive event listener ({passive: true}).
onclick can only hold one handler at a time and overwrites any previously assigned function. addEventListener supports multiple handlers on the same element. It also gives you control over phase (capture vs bubble) and one-time execution via {once: true}.