Static pages are a thing of the past. Users expect applications to respond to every click, keystroke, and swipe. Event listeners are the bridge between your users and your code. They let you say: "When the user clicks this button, run this function." Without events, JavaScript would be blind to user actions.
Anatomy of an event listener
At its core, an event listener is simple: you tell the browser to watch for something, and what to do when it happens.
const button = document.querySelector('#myButton');
button.addEventListener('click', function(event) {
console.log('Button was clicked!');
console.log('Event type:', event.type);
});The three pieces: the element you are watching, the event you are watching for, and the handler function that runs when it happens. The browser automatically passes an event object to your handler with details about what happened.
Common events
| Event | Fires when | Common use case |
|---|---|---|
click | User clicks element | Buttons, links, cards |
dblclick | User double-clicks | Special actions |
mouseenter / mouseleave | Cursor enters/leaves | Hover effects, tooltips |
keydown / keyup | Key pressed/released | Shortcuts, game controls |
input | Input value changes | Live validation, search |
change | Input loses focus with new value | Form validation |
submit | Form submitted | Handle form data |
focus / blur | Element gains/loses focus | Validation, styling |
DOMContentLoaded | DOM fully loaded | Safe to query elements |
Mouse events
card.addEventListener('click', (e) => {
console.log('Clicked at:', e.clientX, e.clientY);
});
card.addEventListener('mouseenter', () => card.classList.add('hovered'));
card.addEventListener('mouseleave', () => card.classList.remove('hovered'));Keyboard events
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
console.log('Enter pressed!');
}
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // prevent browser save dialog
saveDocument();
}
});Form events
form.addEventListener('submit', (e) => {
e.preventDefault(); // stop page reload
const data = new FormData(form);
console.log('Email:', data.get('email'));
});
emailInput.addEventListener('input', (e) => {
console.log('Current value:', e.target.value);
});The event object
The event object is a goldmine of information:
document.addEventListener('click', (event) => {
event.type; // 'click'
event.target; // the element that was clicked
event.currentTarget; // the element with the listener
event.clientX; // mouse X relative to viewport
event.shiftKey; // true if Shift was held
event.preventDefault(); // stop default behavior
event.stopPropagation(); // stop event bubbling
});target vs currentTarget
<div id="container">
<button id="btn">Click me</button>
</div>container.addEventListener('click', (e) => {
e.target; // the button (what was actually clicked)
e.currentTarget; // the container (what has the listener)
});Event bubblingWhat is event bubbling?The default browser behavior where an event triggered on a child element propagates upward through each parent element, firing their handlers along the way. and delegation
When you click an element, the event fires on that element, then on its parent, then its grandparent, all the way up to document. This is called bubbling.
Event delegationWhat is event delegation?Attaching one event listener to a parent element instead of many listeners to each child, using event bubbling to catch events from children. uses bubbling to your advantage, one listener on a parent handles all child events:
// Bad: 100 buttons = 100 listeners
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', handleDelete);
});
// Good: 1 listener handles all buttons (including ones added later)
document.querySelector('#todo-list').addEventListener('click', (e) => {
if (e.target.classList.contains('delete-btn')) {
handleDelete(e.target);
}
});This is better because it uses less memory, works for elements added dynamically after the listener was attached, and is less code to maintain.
addEventListener call should have a corresponding removeEventListener plan, especially in single-page apps where components mount and unmount. Without cleanup, listeners pile up every time a component re-renders, a classic memory leak that degrades performance over time. AI also ignores event delegation entirely. It will generate a forEach loop attaching individual listeners to 50 elements when one delegated listener on the parent would handle all of them, including elements added dynamically in the future. If you see AI adding listeners inside a loop, consider delegation instead. And if the AI-generated component has a React useEffect that adds a listener, always check that it returns a cleanup function, return () => document.removeEventListener(...).Removing event listeners
Listeners stay attached until you remove them. This matters for memory management:
function handleClick() {
console.log('Clicked');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // must be same function referenceAnonymous functions cannot be removed:
// CANNOT be removed later - avoid this pattern
button.addEventListener('click', () => console.log('Clicked'));
// CAN be removed - use named functions
function handler() { console.log('Clicked'); }
button.addEventListener('click', handler);
button.removeEventListener('click', handler);One-time listeners
button.addEventListener('click', () => {
console.log('This runs only once');
}, { once: true });The { once: true } option is the cleanest way to handle one-shot events. The browser removes the listener automatically after the first call, no cleanup needed on your end.
Quick reference
| Task | Code |
|---|---|
| Add listener | el.addEventListener('click', handler) |
| Remove listener | el.removeEventListener('click', handler) |
| Prevent default | event.preventDefault() |
| Stop bubbling | event.stopPropagation() |
| Get clicked element | event.target |
| Get listener element | event.currentTarget |
| One-time listener | el.addEventListener('click', fn, { once: true }) |
| Wait for DOM | document.addEventListener('DOMContentLoaded', fn) |
| Event delegation | Listen on parent, check e.target |