addEventListener
The modern way to register an event handler is addEventListener(type, handler, options). It can attach multiple listeners to the same element for the same event β unlike the older onclick property which overwrites previous handlers.
const btn = document.querySelector('#myBtn');
// Basic usage
btn.addEventListener('click', function(event) {
console.log('Clicked!', event);
});
// Arrow function handler
btn.addEventListener('click', (e) => {
console.log('Also clicked!', e.target);
});
// Named function β required for removeEventListener
function handleClick(e) {
console.log('Named handler:', e.type);
}
btn.addEventListener('click', handleClick);
// removeEventListener β must pass same function reference
btn.removeEventListener('click', handleClick);
// Old way (avoid β overwrites previous handlers)
btn.onclick = () => console.log('Old style');
The Event Object
Every handler receives an Event object as its first argument. It carries information about what happened and methods to control the event's behaviour:
document.querySelector('a').addEventListener('click', function(e) {
// --- Information properties ---
console.log(e.type); // "click"
console.log(e.target); // element that was clicked
console.log(e.currentTarget); // element the listener is attached to
console.log(e.timeStamp); // milliseconds since page load
console.log(e.bubbles); // true for most events
// --- Mouse-specific ---
console.log(e.clientX, e.clientY); // viewport coordinates
console.log(e.button); // 0=left, 1=middle, 2=right
// --- Control methods ---
e.preventDefault(); // stop default browser action (e.g., navigate)
e.stopPropagation(); // stop bubbling to parent elements
e.stopImmediatePropagation(); // also stops other listeners on this element
});
e.target is always the element the user interacted with (deepest clicked element). e.currentTarget is the element whose listener is currently running β equal to this inside a regular function handler.
Event Bubbling and Capturing
When an event fires on a nested element it travels through three phases:
- Capture phase β from
windowdown to the target's parent - Target phase β the target element itself
- Bubble phase β from the target back up to
window
By default, addEventListener registers in the bubble phase. Pass true (or { capture: true }) as the third argument to listen in the capture phase instead.
// HTML: <div id="outer"><div id="inner">Click me</div></div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
// Bubble phase listeners (default)
outer.addEventListener('click', () => console.log('outer bubble'));
inner.addEventListener('click', () => console.log('inner bubble'));
// Capture phase listener
outer.addEventListener('click', () => console.log('outer capture'), true);
// Clicking #inner fires in this order:
// 1. "outer capture" (capture, top-down)
// 2. "inner bubble" (target phase)
// 3. "outer bubble" (bubble, bottom-up)
// stopPropagation β prevents further bubbling
inner.addEventListener('click', (e) => {
e.stopPropagation();
console.log('inner β propagation stopped');
});
outer capture inner bubble outer bubble
Common Event Types
| Category | Events | Notes |
|---|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave, mouseover, mouseout, contextmenu | mouseenter/leave don't bubble; mouseover/out do |
| Keyboard | keydown, keyup, keypress (deprecated) | Use e.key and e.code; keypress is legacy |
| Form | input, change, submit, focus, blur, focusin, focusout, reset | input fires on every keystroke; change fires on commit |
| Window/Document | load, DOMContentLoaded, beforeunload, resize, scroll, hashchange, popstate | DOMContentLoaded fires before images load; load fires after all |
| Touch | touchstart, touchend, touchmove, touchcancel | Mobile; each touch has a touches list |
| Pointer | pointerdown, pointerup, pointermove, pointercancel | Unified mouse + touch + pen API |
Keyboard Events in Practice
document.addEventListener('keydown', (e) => {
console.log(e.key); // "Enter", "a", "ArrowLeft", etc.
console.log(e.code); // "Enter", "KeyA", "ArrowLeft" (physical key)
// Modifier keys
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // prevent browser save dialog
saveDocument();
}
if (e.key === 'Escape') closeModal();
if (e.key === 'Enter') submitForm();
});
// input event β fires on every character change
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', (e) => {
console.log('Current value:', e.target.value);
filterResults(e.target.value);
});
// change event β fires when user commits (leaves field or presses Enter)
searchInput.addEventListener('change', (e) => {
console.log('Committed value:', e.target.value);
});
Listener Options: once, passive, signal
// once: true β auto-removes listener after first invocation
btn.addEventListener('click', handleClick, { once: true });
// passive: true β tells browser handler will never call preventDefault()
// browser can start scrolling immediately (better performance)
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('touchmove', onTouchMove, { passive: true });
// signal β AbortController integration for clean cleanup
const controller = new AbortController();
btn.addEventListener('click', handleClick, { signal: controller.signal });
// Later: cancel all listeners registered with this signal
controller.abort();
// DOMContentLoaded β safe entry point (DOM ready, images may still load)
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM fully parsed');
initApp();
});
Non-passive scroll and touchmove listeners force the browser to wait for your JavaScript before scrolling, causing jank. Always pass { passive: true } unless you specifically need to call preventDefault().
Practical Exercise
ποΈ Practical Exercise
Create a colour-picker keyboard shortcut panel:
- Listen for
keydownon the document. - When the user presses R, G, or B, change the
document.bodybackground to red, green, or blue respectively. - When Escape is pressed, reset to white.
- Display the current key pressed in a
<span>using thee.keyvalue.
π₯ Challenge Exercise
Build a modal dialog that: (1) opens on button click, (2) closes when clicking the backdrop or pressing Escape, (3) traps focus inside the modal while open (Tab cycles only through modal focusable elements), and (4) restores focus to the trigger button on close. Use focusin events and querySelectorAll for focusable selectors.
Summary
π Summary
- Use
addEventListener(type, handler, options)β never overwriteelement.onclick. - The Event object carries
type,target,currentTarget, and control methods. - Events bubble from child β parent β document by default.
- Use
e.stopPropagation()to stop bubbling;e.preventDefault()to cancel default action. - Capture-phase listeners run top-down before bubble-phase listeners.
- Use
{ once: true }for one-shot listeners and{ passive: true }for scroll performance. - Prefer
e.keyovere.keyCode(deprecated) for keyboard events.
Interview Questions
- What is the difference between
e.targetande.currentTarget? - Explain event bubbling. How would you stop it?
- What is the difference between
addEventListenerand assigning toelement.onclick? - When would you use
{ once: true }vsremoveEventListener? - Why should scroll listeners use
{ passive: true }?
Frequently Asked Questions
mouseenter and mouseover? +mouseenter fires only when the pointer enters the element itself β it does NOT bubble and does NOT fire again when moving into a child element. mouseover bubbles and fires each time the pointer enters the element or any of its descendants, which can cause repeated firing in complex layouts.
input and change events? +input fires synchronously on every value change β each keystroke, paste, or cut. change fires only when the user commits the value by blurring the field or pressing Enter. For live validation or search-as-you-type, use input; for final submission logic, use change.
stopPropagation also prevent the default action? +No. stopPropagation() only stops the event from travelling to parent elements. It has no effect on the browser's built-in default action (e.g., navigating on anchor click). To prevent the default action, call e.preventDefault() separately.