JavaScript 62 🧬 Events
Events are the nervous system of a web page. Every click, keystroke, scroll, and network response fires an event, and JavaScript listens. Understanding events — how they propagate, how to listen, how to stop them, and how to delegate — is what separates a static page from a responsive application.
The core of it is addEventListener, but that’s just the entry point. What really matters is knowing how events travel through the DOM (capturing, targeting, bubbling), how to intercept them without breaking the page, and how to handle hundreds of interactive elements without attaching hundreds of listeners.
Key point: Events flow in three phases — capturing (top down), target (the actual element), and bubbling (bottom up). By default, listeners fire during bubbling. Understanding this phase order is the key to predicting when your handlers run.
a – What is an event
An event is a signal that something happened. The browser fires an event when the user interacts with the page, when the DOM changes, when a resource loads, or when a timer completes.
The event object:
Every event handler receives an event object with details about what happened.
button.addEventListener('click', (event) => {
console.log(event.type); // 'click'
console.log(event.target); // the element that was clicked
console.log(event.currentTarget); // the element the listener is on
console.log(event.timeStamp); // when it happened
console.log(event.clientX, event.clientY); // mouse position
});
The event object carries information specific to the event type. A click has coordinates. A keydown has key and code. A form submission has the form data.
event.target vs event.currentTarget:
This is one of the most misunderstood parts of the DOM.
event.target— the actual element that triggered the eventevent.currentTarget— the element the listener is attached to
<div id="outer">
<button id="inner">Click me</button>
</div>
outer.addEventListener('click', (event) => {
console.log(event.target.id); // 'inner' — clicked the button
console.log(event.currentTarget.id); // 'outer' — listener is on outer
});
The user clicked the inner button, so target is the button. But the listener is on the outer div, so currentTarget is the div. This distinction matters for event delegation.
The main event categories:
| Category | Examples | When they fire |
|---|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mouseover, mouseout, mousemove | User interacts with mouse |
| Keyboard | keydown, keyup | User presses/releases a key |
| Form | submit, change, input, focus, blur, reset | Form interaction |
| Document | DOMContentLoaded, load, beforeunload, unload | Page lifecycle |
| Touch | touchstart, touchmove, touchend | Touchscreen interaction |
| Scroll | scroll, wheel | Scrolling |
| Drag | dragstart, drag, dragend, drop | Dragging elements |
| Clipboard | copy, cut, paste | Clipboard operations |
| Media | play, pause, ended, timeupdate | Audio/video |
| Animation | animationstart, animationend, transitionend | CSS animations |
| Custom | CustomEvent('my-event') | User-defined |
Events as objects:
Events are instances of Event or subclasses. The browser creates them, but you can also create custom events.
const event = new Event('build');
element.dispatchEvent(event);
The event lifecycle:
Every event goes through a fixed sequence:
- Capturing phase — event travels from the root (
window) down through ancestors - Target phase — event reaches the actual element
- Bubbling phase — event travels back up through ancestors
By default, addEventListener fires during bubbling. You can opt into capturing.
Why events matter:
- Interaction — respond to user input
- Reactivity — update UI based on state changes
- Decoupling — components communicate through events
- Performance — event delegation handles many elements with one listener
- Lifecycle — hooks into loading, animations, and unload
Registering a listener:
element.addEventListener('click', handler);
You can attach multiple handlers:
element.addEventListener('click', handler1);
element.addEventListener('click', handler2);
// Both run, in order of registration
Removing a listener:
element.removeEventListener('click', handler);
The handler reference must match. Anonymous functions can’t be removed:
element.addEventListener('click', () => {});
element.removeEventListener('click', () => {}); // ❌ does nothing
The old inline way (don’t use it):
<button onclick="alert('hi')">Click</button>
Inline handlers are hard to maintain, mix HTML and JS, and only allow one per element. addEventListener is the right way.
b – Event propagation and phases
Events don’t just fire on one element — they travel through the DOM. Understanding propagation is essential for handling nested elements.
The three phases:
┌──────────────────────────────────────────────┐
│ window │
│ │ │
│ ▼ │
│ CAPTURING PHASE │
│ (root → target) │
│ │ │
│ ▼ │
│ document │
│ │ │
│ ▼ │
│ <html> │
│ │ │
│ ▼ │
│ <body> │
│ │ │
│ ▼ │
│ TARGET PHASE <button> │
│ │ │
│ ▼ │
│ BUBBLING PHASE │
│ (target → root) │
│ │ │
│ ▼ │
│ <body> │
│ │ │
│ ▼ │
│ <html> │
│ │ │
│ ▼ │
│ document │
│ │ │
│ ▼ │
│ window │
│ │
└──────────────────────────────────────────────┘
Capturing — top down:
During capturing, the event travels from window down to the target. Listeners registered with capture: true fire here.
parent.addEventListener('click', handler, { capture: true });
// or
parent.addEventListener('click', handler, true);
Target — the element:
The event reaches the element that triggered it. Both capturing and bubbling listeners on the target fire here (capture first).
Bubbling — bottom up:
By default, events bubble from the target back up through ancestors. This is where most listeners fire.
parent.addEventListener('click', handler);
// capture: false (default)
A demonstration:
<div id="outer">
<div id="inner">
<button id="btn">Click</button>
</div>
</div>
document.querySelector('#outer').addEventListener('click', () => {
console.log('outer capture');
}, true);
document.querySelector('#inner').addEventListener('click', () => {
console.log('inner capture');
}, true);
document.querySelector('#btn').addEventListener('click', () => {
console.log('btn');
});
document.querySelector('#inner').addEventListener('click', () => {
console.log('inner bubble');
});
document.querySelector('#outer').addEventListener('click', () => {
console.log('outer bubble');
});
Clicking the button produces:
[ outer capture ]
[ inner capture ]
[ btn ]
[ inner bubble ]
[ outer bubble ]
The order is exactly as the diagram shows: capture from the outside in, target, bubble from the inside out.
stopPropagation — halt the event:
inner.addEventListener('click', (event) => {
event.stopPropagation();
});
Calling stopPropagation stops the event from moving to the next phase. The current phase’s remaining listeners still run, but no further propagation.
Variants:
| Method | Effect |
|---|---|
event.stopPropagation() | Stops propagation, current listeners run |
event.stopImmediatePropagation() | Stops immediately — no more listeners at all |
event.cancelBubble = true | Legacy, same as stopPropagation |
preventDefault — stop default behavior:
Different from stopPropagation. preventDefault stops the browser’s built-in behavior — following a link, submitting a form, checking a checkbox.
form.addEventListener('submit', (event) => {
event.preventDefault();
// handle submission manually
});
| Method | Stops |
|---|---|
stopPropagation | Event propagation |
preventDefault | Default action |
They’re independent:
link.addEventListener('click', (event) => {
event.preventDefault(); // don't navigate
event.stopPropagation(); // don't bubble
});
once option — auto-remove:
button.addEventListener('click', handler, { once: true });
The listener runs once and is automatically removed.
passive option — better scroll performance:
element.addEventListener('scroll', handler, { passive: true });
Tells the browser the handler won’t call preventDefault. Enables scrolling optimizations.
Common listener options:
| Option | Default | Effect |
|---|---|---|
capture | false | Fire during capturing |
once | false | Auto-remove after firing |
passive | false | Promise not to preventDefault |
signal | — | AbortController support |
Aborting listeners with AbortController:
const controller = new AbortController();
element.addEventListener('click', handler, {
signal: controller.signal
});
// Later
controller.abort(); // removes all listeners using this signal
Clean way to remove many listeners at once.
Non-bubbling events:
Some events don’t bubble — focus, blur, load, mouseenter, mouseleave.
input.addEventListener('focus', handler);
// Fires only on the input, not on ancestors
For delegation on these, use capture phase:
document.addEventListener('focus', handler, true);
Custom events:
const event = new CustomEvent('user-login', {
detail: { userId: 42 },
bubbles: true,
cancelable: true
});
element.dispatchEvent(event);
Listen for them like any other:
element.addEventListener('user-login', (event) => {
console.log(event.detail.userId); // 42
});
Event delegation — the powerful pattern:
Attach one listener to a parent instead of many to children.
<ul id="list">
<li><button class="del" data-id="1">Delete</button></li>
<li><button class="del" data-id="2">Delete</button></li>
<!-- 100 more -->
</ul>
document.querySelector('#list').addEventListener('click', (event) => {
const button = event.target.closest('.del');
if (!button) return;
deleteItem(button.dataset.id);
});
One listener handles all 100+ buttons — existing and future.
Why delegation works:
Events bubble from the target up to ancestors. A listener on the parent catches events from any descendant.
Delegation with closest:
list.addEventListener('click', (event) => {
const item = event.target.closest('li');
if (!item) return;
item.classList.toggle('selected');
});
closest walks up from target to find the matching ancestor.
Delegation caveats:
- Event must bubble (or use capture)
event.targetmay be a nested child — useclosest- Can’t use
stopPropagationinside if you want the parent to hear it - Not suitable for non-bubbling events without capture
c – Common event patterns
These patterns come up in almost every web application.
Pattern 1 — Debounce input events:
Input events fire on every keystroke. Debouncing waits until typing stops.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
search.addEventListener('input', debounce((event) => {
console.log('Search for:', event.target.value);
}, 300));
The handler runs 300ms after the last keystroke — not on every one.
Pattern 2 — Throttle scroll events:
Scroll fires dozens of times per second. Throttling limits how often the handler runs.
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn(...args);
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log('Scrolled');
}, 100));
Pattern 3 — Form submission with Fetch:
form.addEventListener('submit', async (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
try {
const response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Request failed');
const result = await response.json();
console.log('Success:', result);
} catch (err) {
console.error('Error:', err);
}
});
Pattern 4 — Click outside to close:
function onClickOutside(element, callback) {
document.addEventListener('click', (event) => {
if (!element.contains(event.target)) {
callback();
}
});
}
onClickOutside(modal, () => closeModal());
The contains check determines if the click was inside the element.
Pattern 5 — Keyboard shortcuts:
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closeModal();
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
save();
}
if (event.metaKey && event.key === 'k') {
event.preventDefault();
openSearch();
}
});
Pattern 6 — Drag and drop:
draggable.addEventListener('dragstart', (event) => {
event.dataTransfer.setData('text/plain', event.target.id);
});
dropZone.addEventListener('dragover', (event) => {
event.preventDefault(); // required to allow drop
});
dropZone.addEventListener('drop', (event) => {
event.preventDefault();
const id = event.dataTransfer.getData('text/plain');
console.log('Dropped:', id);
});
Pattern 7 — Toggle active state:
tabs.addEventListener('click', (event) => {
const tab = event.target.closest('.tab');
if (!tab) return;
tabs.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
Pattern 8 — Hover with mouseenter/mouseleave:
card.addEventListener('mouseenter', () => {
card.classList.add('hovered');
});
card.addEventListener('mouseleave', () => {
card.classList.remove('hovered');
});
mouseenter/mouseleave don’t bubble — no accidental triggers from children.
Pattern 9 — Input validation on blur:
input.addEventListener('blur', () => {
const value = input.value.trim();
if (value && !isValid(value)) {
input.classList.add('error');
} else {
input.classList.remove('error');
}
});
Fires when the input loses focus — a natural validation point.
Pattern 10 — Custom events for component communication:
// Dispatcher
class Modal {
open() {
this.element.dispatchEvent(new CustomEvent('modal-open', {
bubbles: true,
detail: { id: this.id }
}));
}
}
// Listener
document.addEventListener('modal-open', (event) => {
console.log('Modal opened:', event.detail.id);
});
Custom events let components communicate without direct references.
Pattern 11 — Clean up listeners:
class Widget {
constructor(element) {
this.element = element;
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener('click', this.handleClick);
}
handleClick(event) {
console.log('clicked');
}
destroy() {
this.element.removeEventListener('click', this.handleClick);
}
}
Bound references are essential for cleanup — you need the same function reference.
Pattern 12 — Observer for lazy loading:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
The image loads only when it enters the viewport.
Pattern 13 — MutationObserver for dynamic content:
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
console.log('Children changed:', mutation.addedNodes);
}
}
});
observer.observe(container, { childList: true, subtree: true });
Pattern 14 — Async event handler with race protection:
let currentRequest = 0;
input.addEventListener('input', async (event) => {
const id = ++currentRequest;
const results = await fetchResults(event.target.value);
if (id !== currentRequest) return; // stale response
renderResults(results);
});
Prevents older async responses from overwriting newer ones.
Pattern 15 — Passive scroll for performance:
window.addEventListener('scroll', () => {
updateProgressBar();
}, { passive: true });
Tells the browser the handler won’t block scrolling — enables optimizations.
Complete Example Session
// ============================================
// PART 1: BASIC LISTENER
// ============================================
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log('clicked');
});
// [ clicked ] (when button is clicked)
// ============================================
// PART 2: EVENT OBJECT
// ============================================
button.addEventListener('click', (event) => {
console.log(event.type); // 'click'
console.log(event.target); // the button element
console.log(event.timeStamp); // timestamp in ms
});
// [ click ]
// [ <button>...</button> ]
// [ 1234.567 ]
// ============================================
// PART 3: TARGET VS CURRENTTARGET
// ============================================
const outer = document.querySelector('#outer');
const inner = document.querySelector('#inner');
outer.addEventListener('click', (event) => {
console.log('target:', event.target.id);
console.log('currentTarget:', event.currentTarget.id);
});
// Click on inner:
// [ target: inner ]
// [ currentTarget: outer ]
// ============================================
// PART 4: MULTIPLE LISTENERS
// ============================================
button.addEventListener('click', () => console.log('first'));
button.addEventListener('click', () => console.log('second'));
// Click:
// [ first ]
// [ second ]
// ============================================
// PART 5: REMOVE LISTENER
// ============================================
function handler() {
console.log('once');
}
button.addEventListener('click', handler);
button.removeEventListener('click', handler);
// ============================================
// PART 6: ONCE OPTION
// ============================================
button.addEventListener('click', () => {
console.log('only once');
}, { once: true });
// ============================================
// PART 7: PREVENT DEFAULT
// ============================================
const form = document.querySelector('form');
form.addEventListener('submit', (event) => {
event.preventDefault();
console.log('handled manually');
});
// Form doesn't reload the page
// ============================================
// PART 8: STOP PROPAGATION
// ============================================
inner.addEventListener('click', (event) => {
event.stopPropagation();
console.log('stopped');
});
outer.addEventListener('click', () => {
console.log('never fires');
});
// Click inner:
// [ stopped ] only
// ============================================
// PART 9: CAPTURING
// ============================================
outer.addEventListener('click', () => {
console.log('capture');
}, true);
inner.addEventListener('click', () => {
console.log('bubble');
});
// Click inner:
// [ capture ]
// [ bubble ]
// ============================================
// PART 10: EVENT DELEGATION
// ============================================
const list = document.querySelector('#list');
list.addEventListener('click', (event) => {
const button = event.target.closest('.del');
if (!button) return;
console.log('delete', button.dataset.id);
});
// ============================================
// PART 11: DEBOUNCE
// ============================================
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const search = document.querySelector('#search');
search.addEventListener('input', debounce((event) => {
console.log('Searching:', event.target.value);
}, 300));
// ============================================
// PART 12: THROTTLE
// ============================================
function throttle(fn, interval) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn(...args);
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log('scroll');
}, 100));
// ============================================
// PART 13: CUSTOM EVENT
// ============================================
const customEvent = new CustomEvent('user-login', {
detail: { userId: 42 },
bubbles: true
});
document.addEventListener('user-login', (event) => {
console.log('User:', event.detail.userId);
});
document.dispatchEvent(customEvent);
// [ User: 42 ]
// ============================================
// PART 14: KEYBOARD
// ============================================
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
console.log('escape pressed');
}
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('saving');
}
});
// ============================================
// PART 15: ABORT CONTROLLER
// ============================================
const controller = new AbortController();
button.addEventListener('click', () => {
console.log('handler');
}, { signal: controller.signal });
// Later
controller.abort(); // removes listener
// ============================================
// PART 16: CLICK OUTSIDE
// ============================================
function onClickOutside(element, callback) {
document.addEventListener('click', (event) => {
if (!element.contains(event.target)) {
callback();
}
});
}
// ============================================
// PART 17: INTERSECTION OBSERVER
// ============================================
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('visible:', entry.target.id);
}
});
});
// ============================================
// PART 18: MUTATION OBSERVER
// ============================================
const mutObserver = new MutationObserver((mutations) => {
console.log('changes:', mutations.length);
});
mutObserver.observe(document.body, { childList: true, subtree: true });
// ============================================
// PART 19: CLEANUP
// ============================================
class Component {
constructor(el) {
this.el = el;
this.handleClick = this.handleClick.bind(this);
this.el.addEventListener('click', this.handleClick);
}
handleClick() {
console.log('clicked');
}
destroy() {
this.el.removeEventListener('click', this.handleClick);
}
}
// ============================================
// PART 20: FULL EXAMPLE
// ============================================
const searchInput = document.querySelector('#search');
const results = document.querySelector('#results');
let requestId = 0;
searchInput.addEventListener('input', debounce(async (event) => {
const id = ++requestId;
const query = event.target.value.trim();
if (!query) {
results.innerHTML = '';
return;
}
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (id !== requestId) return; // stale
results.innerHTML = data.items
.map(item => `<li>${item.name}</li>`)
.join('');
} catch (err) {
console.error('Search failed:', err);
}
}, 300));
Quick Reference
Adding and Removing
| Method | Purpose |
|---|---|
addEventListener(type, fn, options) | Attach |
removeEventListener(type, fn, options) | Detach |
dispatchEvent(event) | Fire |
Common Events
| Category | Events |
|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave, mouseover, mouseout |
| Keyboard | keydown, keyup |
| Form | submit, change, input, focus, blur, reset |
| Document | DOMContentLoaded, load, beforeunload |
| Touch | touchstart, touchmove, touchend |
| Scroll | scroll, wheel |
| Drag | dragstart, drag, dragover, drop, dragend |
Event Object
| Property | Meaning |
|---|---|
type | Event name |
target | Triggering element |
currentTarget | Listener element |
timeStamp | When it fired |
detail | Custom data (CustomEvent) |
key | Key name (keyboard) |
clientX, clientY | Mouse coordinates |
Event Methods
| Method | Effect |
|---|---|
preventDefault() | Stop default action |
stopPropagation() | Stop bubbling/capturing |
stopImmediatePropagation() | Stop all listeners |
composedPath() | Event path |
Listener Options
| Option | Default | Purpose |
|---|---|---|
capture | false | Fire during capture |
once | false | Auto-remove |
passive | false | Won’t preventDefault |
signal | — | AbortController signal |
Propagation Phases
| Phase | Direction | Listeners |
|---|---|---|
| Capturing | window → target | capture: true |
| Target | On target | Both |
| Bubbling | target → window | capture: false (default) |
Non-Bubbling Events
| Event | Bubbles? |
|---|---|
focus / blur | ❌ |
load | ❌ |
mouseenter / mouseleave | ❌ |
DOMContentLoaded | ❌ |
click | ✅ |
keydown | ✅ |
submit | ✅ |
Delegation
| Step | Code |
|---|---|
| Attach to parent | parent.addEventListener('click', fn) |
| Check target | event.target.matches(sel) |
| Find nearest ancestor | event.target.closest(sel) |
Best Practices
✅ Do This:
// Use addEventListener, not inline onclick
button.addEventListener('click', handler); // ✅
// Use event delegation for lists
list.addEventListener('click', handler); // ✅
// Use closest for delegation
const item = event.target.closest('li'); // ✅
// Use once for one-time handlers
button.addEventListener('click', fn, { once: true }); // ✅
// Use passive for scroll
window.addEventListener('scroll', fn, { passive: true }); // ✅
// Clean up listeners
el.removeEventListener('click', handler); // ✅
// Use AbortController for bulk cleanup
const c = new AbortController();
el.addEventListener('click', fn, { signal: c.signal });
c.abort(); // ✅
// Debounce input handlers
input.addEventListener('input', debounce(fn, 300)); // ✅
// Bind handlers for removal
this.handleClick = this.handleClick.bind(this); // ✅
❌ Don’t Do This:
// Don't use inline onclick
<button onclick="doThing()"> // ❌
// Don't attach listener per item
items.forEach(i => i.addEventListener(...)); // ⚠️ use delegation
// Don't forget to remove listeners
// (causes memory leaks) // ❌
// Don't use anonymous functions if you need to remove
el.addEventListener('click', () => {}); // ❌ can't remove
el.removeEventListener('click', () => {}); // ❌ different ref
// Don't use stopPropagation liberally
event.stopPropagation(); // ⚠️ breaks other handlers
// Don't preventDefault on passive
el.addEventListener('scroll', (e) => {
e.preventDefault(); // ⚠️ ignored if passive
}, { passive: true }); // ❌
// Don't ignore once/AbortController for cleanup
// Repeated adds without removes → leaks // ❌
// Don't rely on event.target for delegation without closest
if (event.target.classList.contains('x')); // ⚠️ breaks on nested
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Anonymous handler removal | Can’t remove | Named function |
| Forgetting to remove | Memory leak | Remove on cleanup |
stopPropagation everywhere | Breaks other listeners | Use sparingly |
preventDefault on passive | Ignored | Remove passive |
event.target is nested | Wrong element | Use closest |
| Non-bubbling events | Delegation fails | Use capture |
| Too many listeners | Performance | Delegate |
| Race conditions in async | Stale response | Compare IDs |
this in handlers | Lost context | Arrow or bind |
| Not binding for removal | Wrong reference | Bind once, store |
Real-World Examples
1. Click Handler
button.addEventListener('click', () => console.log('clicked'));
Fires when the button is clicked. Simple, one-time binding.
2. Event Object
button.addEventListener('click', (event) => {
console.log(event.target);
});
Access details about the event — target element, coordinates, timestamp.
3. Prevent Form Submission
form.addEventListener('submit', (event) => {
event.preventDefault();
handleSubmit();
});
Stops the page from reloading on submit — standard for single-page apps.
4. Stop Propagation
child.addEventListener('click', (event) => {
event.stopPropagation();
});
Prevents the parent from also receiving the event.
5. Capturing
parent.addEventListener('click', handler, true);
Fires before the target’s own handlers, during the capturing phase.
6. Event Delegation
list.addEventListener('click', (event) => {
const item = event.target.closest('li');
if (item) item.classList.toggle('done');
});
One listener handles all list items, including future ones.
7. Debounce Input
input.addEventListener('input', debounce(fn, 300));
Waits for a pause in typing before firing.
8. Throttle Scroll
window.addEventListener('scroll', throttle(fn, 100));
Limits how often the handler runs during scrolling.
9. Keyboard Shortcut
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
save();
}
});
Listens for Ctrl+S and prevents the browser’s save dialog.
10. Custom Event
el.dispatchEvent(new CustomEvent('done', {
detail: { id: 42 }
}));
Components communicate through custom events.
11. Once Option
button.addEventListener('click', fn, { once: true });
Auto-removes after the first click — useful for one-time setup.
12. Passive Scroll
window.addEventListener('scroll', updateProgress, { passive: true });
Tells the browser the handler won’t block scrolling — improves performance.
13. Abort Controller
const controller = new AbortController();
el.addEventListener('click', fn, { signal: controller.signal });
controller.abort();
Removes all listeners tied to the controller in one call.
14. Click Outside
document.addEventListener('click', (event) => {
if (!modal.contains(event.target)) closeModal();
});
Detects clicks outside an element — common for dropdowns and modals.
15. Intersection Observer
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) e.target.classList.add('visible');
});
});
observer.observe(el);
Fires when elements enter or leave the viewport — used for lazy loading and animations.
16. Mutation Observer
const observer = new MutationObserver(mutations => {
console.log('changed');
});
observer.observe(target, { childList: true });
Fires when the DOM under target changes.
17. Drag and Drop
draggable.addEventListener('dragstart', (event) => {
event.dataTransfer.setData('text', event.target.id);
});
Standard pattern for drag-and-drop.
18. Async with Race Protection
let id = 0;
input.addEventListener('input', async () => {
const currentId = ++id;
const result = await search(input.value);
if (currentId !== id) return;
render(result);
});
Ignores stale responses from earlier requests.
19. Cleanup on Destroy
destroy() {
this.el.removeEventListener('click', this.handleClick);
this.observer.disconnect();
}
Prevents memory leaks when components unmount.
20. Full Search Example
const searchInput = document.querySelector('#search');
const results = document.querySelector('#results');
let requestId = 0;
searchInput.addEventListener('input', debounce(async (event) => {
const id = ++requestId;
const query = event.target.value.trim();
if (!query) {
results.innerHTML = '';
return;
}
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (id !== requestId) return;
results.innerHTML = data.items
.map(item => `<li>${item.name}</li>`)
.join('');
} catch (err) {
console.error('Search failed:', err);
}
}, 300));
Debounced input handler that fetches results and ignores stale responses.
Visual: Event Flow
┌──────────────────────────────────────────────┐
│ User clicks a button nested in <div> │
│ │
│ 1. window │
│ │ │
│ ▼ │
│ document ← capturing phase │
│ │ │
│ ▼ │
│ <html> │
│ │ │
│ ▼ │
│ <body> │
│ │ │
│ ▼ │
│ <div> ← parent listener (capture) │
│ │ │
│ ▼ │
│ <button> ← target phase │
│ │ │
│ ▼ │
│ <div> ← parent listener (bubble) │
│ │ │
│ ▼ │
│ <body> │
│ │ │
│ ▼ │
│ <html> │
│ │ │
│ ▼ │
│ document │
│ │ │
│ ▼ │
│ window ← bubbling phase │
│ │
└──────────────────────────────────────────────┘
Visual: stopPropagation vs preventDefault
┌──────────────────────────────────────────────┐
│ stopPropagation │
│ │
│ Stops the event from reaching ancestors │
│ Current handlers still run │
│ │
│ child ──► parent ✗ │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ preventDefault │
│ │
│ Stops the browser's default action │
│ Event still propagates normally │
│ │
│ <a href> → no navigation │
│ <form> → no submission │
│ checkbox → no toggle │
│ keydown → no default action │
│ │
└──────────────────────────────────────────────┘
Visual: Event Delegation
┌──────────────────────────────────────────────┐
│ Without delegation │
│ │
│ <ul> │
│ <li> <button> ← listener #1 │
│ <li> <button> ← listener #2 │
│ <li> <button> ← listener #3 │
│ ... 97 more ← 97 more listeners │
│ </ul> │
│ │
│ ❌ 100 listeners, slow to add/remove │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With delegation │
│ │
│ <ul> ← one listener │
│ <li> <button> │
│ <li> <button> │
│ <li> <button> │
│ ... 97 more │
│ </ul> │
│ │
│ ✅ 1 listener handles everything │
│ │
└──────────────────────────────────────────────┘
Visual: Debounce vs Throttle
┌──────────────────────────────────────────────┐
│ Input events (typing) │
│ │
│ a a a a a b b b c c c │
│ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ │
│ │
│ Every keystroke fires a handler │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Debounce (300ms) │
│ │
│ a a a a a b b b c c c [wait 300ms] │
│ ▲ │
│ fires once, at end │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Throttle (100ms) │
│ │
│ a a a a a b b b c c c │
│ ▲ ▲ ▲ │
│ │
│ fires at most every 100ms │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Method | Example |
|---|---|---|
| Listen | addEventListener | el.addEventListener('click', fn) |
| Remove | removeEventListener | el.removeEventListener('click', fn) |
| Dispatch | dispatchEvent | el.dispatchEvent(event) |
| Default | preventDefault | event.preventDefault() |
| Propagate | stopPropagation | event.stopPropagation() |
| Target | event.target | Triggering element |
| Listener | event.currentTarget | Listener element |
| Delegation | Parent listener | parent.addEventListener |
| Once | { once: true } | Auto-remove |
| Passive | { passive: true } | Scroll perf |
| Capture | { capture: true } | Capturing phase |
| Signal | { signal } | AbortController |
| Custom | CustomEvent | User-defined events |
Key takeaways:
- Events flow in three phases: capture, target, bubble
addEventListeneris the standard — never use inlineonclickevent.targetis the triggered element;event.currentTargetis the listener elementpreventDefaultstops the browser default;stopPropagationstops propagation- Event delegation — one parent listener handles many children via bubbling
- Use
event.target.closest(sel)to find the right ancestor during delegation once,passive,capture, andsignalare listener options worth knowing- Non-bubbling events (
focus,blur,mouseenter) need capture for delegation - Debounce and throttle control how often high-frequency events fire
AbortControllerremoves many listeners in one callIntersectionObserver,MutationObserver,ResizeObserverreplace polling with reactive hooks- Always clean up listeners to prevent memory leaks
Remember: Events are how your page reacts. Listen with addEventListener, delegate when you have many elements, and always clean up. Understand the three phases — capturing, target, bubbling — so you know when your handlers fire. Prevent the default when you’re taking over, and stop propagation sparingly. Debounce input, throttle scroll, and use observers for visibility and mutations. Master events, and your UI responds to the user without a hitch.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!