Mouse Events
Handle various mouse interactions from clicks to movement.
const element = document.querySelector(".interactive");
// Click events
element.addEventListener("click", (e) => {
console.log("Single click");
});
element.addEventListener("dblclick", (e) => {
console.log("Double click");
});
element.addEventListener("contextmenu", (e) => {
e.preventDefault(); // Prevent context menu
console.log("Right click");
});
// Mouse button info
element.addEventListener("mousedown", (e) => {
console.log("Button pressed:", e.button);
// 0 = left, 1 = middle, 2 = right
});
element.addEventListener("mouseup", (e) => {
console.log("Button released");
});
// Mouse enter/leave (don't bubble)
element.addEventListener("mouseenter", () => {
element.classList.add("hovered");
});
element.addEventListener("mouseleave", () => {
element.classList.remove("hovered");
});
// Mouse over/out (bubble, fire on children too)
element.addEventListener("mouseover", (e) => {
console.log("Over:", e.target);
});
// Mouse movement
element.addEventListener("mousemove", (e) => {
// Coordinates relative to viewport
console.log(`Client: ${e.clientX}, ${e.clientY}`);
// Coordinates relative to page
console.log(`Page: ${e.pageX}, ${e.pageY}`);
// Coordinates relative to element
console.log(`Offset: ${e.offsetX}, ${e.offsetY}`);
// Screen coordinates
console.log(`Screen: ${e.screenX}, ${e.screenY}`);
});
// Practical: Follow cursor
document.addEventListener("mousemove", (e) => {
const cursor = document.querySelector(".cursor");
cursor.style.left = e.clientX + "px";
cursor.style.top = e.clientY + "px";
});
// Drag detection
let isDragging = false;
let startPos = { x: 0, y: 0 };
element.addEventListener("mousedown", (e) => {
isDragging = true;
startPos = { x: e.clientX, y: e.clientY };
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;
console.log(`Dragged: ${dx}, ${dy}`);
});
document.addEventListener("mouseup", () => {
isDragging = false;
});
Keyboard Events
Respond to keyboard input for shortcuts, games, and form handling.
// keydown - fires when key is pressed (repeats if held)
document.addEventListener("keydown", (e) => {
console.log("Key:", e.key); // "a", "Enter", "ArrowUp"
console.log("Code:", e.code); // "KeyA", "Enter", "ArrowUp"
console.log("Repeat:", e.repeat); // true if key is held
});
// keyup - fires when key is released
document.addEventListener("keyup", (e) => {
console.log("Released:", e.key);
});
// Modifier keys
document.addEventListener("keydown", (e) => {
console.log("Ctrl:", e.ctrlKey);
console.log("Shift:", e.shiftKey);
console.log("Alt:", e.altKey);
console.log("Meta:", e.metaKey); // Cmd on Mac, Win on Windows
});
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
// Ctrl/Cmd + S
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
saveDocument();
}
// Ctrl/Cmd + Shift + P
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "P") {
e.preventDefault();
openCommandPalette();
}
// Escape key
if (e.key === "Escape") {
closeModal();
}
});
// Key navigation
const focusableItems = document.querySelectorAll(".menu-item");
let currentIndex = 0;
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
currentIndex = Math.min(currentIndex + 1, focusableItems.length - 1);
focusableItems[currentIndex].focus();
}
if (e.key === "ArrowUp") {
e.preventDefault();
currentIndex = Math.max(currentIndex - 1, 0);
focusableItems[currentIndex].focus();
}
});
// Input specific events
const input = document.querySelector("input");
input.addEventListener("keydown", (e) => {
// Prevent certain characters
if (e.key === " ") {
e.preventDefault(); // No spaces allowed
}
});
// Difference: key vs code
// key = character ("a" or "A" based on Shift)
// code = physical key ("KeyA" regardless of Shift)
document.addEventListener("keydown", (e) => {
if (e.code === "KeyW") {
// Always W key, regardless of layout or shift
movePlayer("up");
}
});
The keypress event is deprecated. Use keydown for most cases, or the input event for text input changes.
Form Events
Handle form submissions and input changes effectively.
const form = document.querySelector("form");
const input = document.querySelector("input");
const select = document.querySelector("select");
// Form submit
form.addEventListener("submit", (e) => {
e.preventDefault(); // Prevent page reload
// Get form data
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log(data);
// Or access individual fields
console.log(formData.get("email"));
});
// Input event - fires on every change
input.addEventListener("input", (e) => {
console.log("Current value:", e.target.value);
// Great for real-time validation or search
});
// Change event - fires when value is "committed"
// (on blur for text, immediately for checkboxes/selects)
input.addEventListener("change", (e) => {
console.log("Value changed to:", e.target.value);
});
select.addEventListener("change", (e) => {
console.log("Selected:", e.target.value);
});
// Focus events
input.addEventListener("focus", () => {
console.log("Input focused");
input.parentElement.classList.add("focused");
});
input.addEventListener("blur", () => {
console.log("Input blurred");
input.parentElement.classList.remove("focused");
validateInput(input);
});
// focusin/focusout - same but bubble
form.addEventListener("focusin", (e) => {
console.log("Focused:", e.target.name);
});
// Real-time validation
input.addEventListener("input", (e) => {
const value = e.target.value;
const isValid = value.length >= 3;
input.classList.toggle("invalid", !isValid);
input.classList.toggle("valid", isValid);
});
// Form reset
form.addEventListener("reset", (e) => {
// Optionally prevent or add custom logic
console.log("Form was reset");
});
// Getting all form values
function getFormValues(form) {
const formData = new FormData(form);
const values = {};
for (const [key, value] of formData.entries()) {
values[key] = value;
}
return values;
}
// Checkbox handling
const checkbox = document.querySelector('input[type="checkbox"]');
checkbox.addEventListener("change", (e) => {
console.log("Checked:", e.target.checked);
});
// Radio button handling
const radios = document.querySelectorAll('input[name="choice"]');
radios.forEach(radio => {
radio.addEventListener("change", (e) => {
if (e.target.checked) {
console.log("Selected:", e.target.value);
}
});
});
Event Propagation
Understand how events travel through the DOM tree.
// Event phases:
// 1. Capturing - event travels DOWN from document to target
// 2. Target - event reaches the target element
// 3. Bubbling - event travels UP from target to document
// <div class="outer">
// <div class="inner">
// <button>Click me</button>
// </div>
// </div>
const outer = document.querySelector(".outer");
const inner = document.querySelector(".inner");
const button = document.querySelector("button");
// Default: bubbling phase (bottom to top)
outer.addEventListener("click", () => console.log("Outer"));
inner.addEventListener("click", () => console.log("Inner"));
button.addEventListener("click", () => console.log("Button"));
// Click button logs: "Button", "Inner", "Outer"
// Capturing phase (top to bottom)
outer.addEventListener("click", () => console.log("Outer (capture)"), true);
// or: { capture: true }
// Click button logs: "Outer (capture)", "Button", "Inner", "Outer"
// Stop propagation - prevent event from continuing
button.addEventListener("click", (e) => {
e.stopPropagation();
console.log("Button clicked");
// Parent handlers won't fire
});
// Stop immediate propagation - stop ALL handlers
button.addEventListener("click", (e) => {
e.stopImmediatePropagation();
console.log("First handler");
});
button.addEventListener("click", () => {
console.log("Second handler"); // Won't run!
});
// Practical example: modal closing
const modal = document.querySelector(".modal");
const modalContent = document.querySelector(".modal-content");
modal.addEventListener("click", () => {
// Close when clicking backdrop
closeModal();
});
modalContent.addEventListener("click", (e) => {
// Don't close when clicking content
e.stopPropagation();
});
// Check event phase
document.addEventListener("click", (e) => {
console.log("Phase:", e.eventPhase);
// 1 = capturing, 2 = target, 3 = bubbling
});
Most event handling uses bubbling (default). Use capturing only when you need to intercept events before they reach their target, like for global keyboard shortcuts.
Custom Events
Create and dispatch your own events for component communication.
// Create a custom event
const event = new Event("myEvent");
// CustomEvent for passing data
const customEvent = new CustomEvent("userLoggedIn", {
detail: {
userId: 123,
username: "john_doe"
},
bubbles: true, // Allow bubbling
cancelable: true // Allow preventDefault
});
// Listen for custom event
document.addEventListener("userLoggedIn", (e) => {
console.log("User logged in:", e.detail.username);
console.log("User ID:", e.detail.userId);
});
// Dispatch the event
document.dispatchEvent(customEvent);
// Practical: Component communication
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
// Dispatch event when cart changes
document.dispatchEvent(new CustomEvent("cartUpdated", {
detail: {
items: this.items,
total: this.getTotal()
},
bubbles: true
}));
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// UI listens for cart updates
document.addEventListener("cartUpdated", (e) => {
const cartBadge = document.querySelector(".cart-badge");
cartBadge.textContent = e.detail.items.length;
const totalDisplay = document.querySelector(".cart-total");
totalDisplay.textContent = `$${e.detail.total}`;
});
// Cancelable custom events
const beforeDelete = new CustomEvent("itemBeforeDelete", {
detail: { itemId: 5 },
cancelable: true
});
// Handler can prevent the action
document.addEventListener("itemBeforeDelete", (e) => {
const confirmDelete = confirm("Are you sure?");
if (!confirmDelete) {
e.preventDefault();
}
});
// Check if event was cancelled
const wasCancelled = !document.dispatchEvent(beforeDelete);
if (!wasCancelled) {
deleteItem(5);
}
// Event emitter pattern
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
off(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(callback => callback(data));
}
}
const emitter = new EventEmitter();
emitter.on("message", (data) => console.log("Received:", data));
emitter.emit("message", { text: "Hello!" });
Window and Document Events
Handle page lifecycle, scrolling, and resizing events.
// Page load events
window.addEventListener("DOMContentLoaded", () => {
// DOM is ready, but images/stylesheets may still load
console.log("DOM ready!");
});
window.addEventListener("load", () => {
// Everything loaded (images, styles, etc.)
console.log("Page fully loaded!");
});
// Before page unload
window.addEventListener("beforeunload", (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = ""; // Required for Chrome
// Browser will show confirmation dialog
}
});
// Window resize
window.addEventListener("resize", () => {
console.log(`Window: ${window.innerWidth} x ${window.innerHeight}`);
});
// Debounced resize (for performance)
let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
console.log("Resize finished");
handleResize();
}, 250);
});
// Scroll events
window.addEventListener("scroll", () => {
console.log("Scroll Y:", window.scrollY);
console.log("Scroll X:", window.scrollX);
});
// Throttled scroll (for performance)
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
});
// Scroll-triggered animations
function handleScroll() {
const header = document.querySelector("header");
if (window.scrollY > 100) {
header.classList.add("scrolled");
} else {
header.classList.remove("scrolled");
}
}
// Visibility change (tab hidden/shown)
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
console.log("Tab is hidden");
pauseVideo();
} else {
console.log("Tab is visible");
resumeVideo();
}
});
// Online/offline detection
window.addEventListener("online", () => {
console.log("Back online!");
showNotification("Connection restored");
});
window.addEventListener("offline", () => {
console.log("Lost connection");
showNotification("You are offline");
});
// Hashchange (URL hash changes)
window.addEventListener("hashchange", (e) => {
console.log("Old URL:", e.oldURL);
console.log("New URL:", e.newURL);
console.log("Hash:", location.hash);
});
// Popstate (browser back/forward)
window.addEventListener("popstate", (e) => {
console.log("Navigation state:", e.state);
});
Touch Events
Handle touch interactions for mobile devices.
const element = document.querySelector(".touch-area");
// Touch events
element.addEventListener("touchstart", (e) => {
const touch = e.touches[0];
console.log(`Touch started at: ${touch.clientX}, ${touch.clientY}`);
e.preventDefault(); // Prevent scrolling if needed
});
element.addEventListener("touchmove", (e) => {
const touch = e.touches[0];
console.log(`Touch moved to: ${touch.clientX}, ${touch.clientY}`);
});
element.addEventListener("touchend", (e) => {
const touch = e.changedTouches[0];
console.log(`Touch ended at: ${touch.clientX}, ${touch.clientY}`);
});
element.addEventListener("touchcancel", (e) => {
console.log("Touch cancelled");
});
// Multi-touch
element.addEventListener("touchstart", (e) => {
console.log(`Number of fingers: ${e.touches.length}`);
// Pinch detection
if (e.touches.length === 2) {
const distance = getDistance(e.touches[0], e.touches[1]);
console.log("Initial pinch distance:", distance);
}
});
function getDistance(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
// Swipe detection
let startX, startY;
element.addEventListener("touchstart", (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
});
element.addEventListener("touchend", (e) => {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const dx = endX - startX;
const dy = endY - startY;
const threshold = 50;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > threshold) {
if (dx > 0) {
console.log("Swiped right");
} else {
console.log("Swiped left");
}
} else if (Math.abs(dy) > threshold) {
if (dy > 0) {
console.log("Swiped down");
} else {
console.log("Swiped up");
}
}
});
// Pointer events (work for both mouse AND touch)
element.addEventListener("pointerdown", (e) => {
console.log("Pointer type:", e.pointerType); // "mouse", "touch", "pen"
console.log("Position:", e.clientX, e.clientY);
});
element.addEventListener("pointermove", (e) => {
// Works for mouse and touch
});
element.addEventListener("pointerup", (e) => {
// End of interaction
});
Advanced Custom Events
Custom events enable loosely coupled components to communicate without direct dependencies. They're essential for building modular, maintainable applications.
Event Emitter Pattern
// Reusable EventEmitter class
class EventEmitter {
#events = new Map();
on(event, listener, options = {}) {
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
const listeners = this.#events.get(event);
const wrapper = options.once
? (...args) => {
this.off(event, wrapper);
listener.apply(this, args);
}
: listener;
listeners.push({ fn: wrapper, original: listener });
return this;
}
once(event, listener) {
return this.on(event, listener, { once: true });
}
off(event, listener) {
if (!this.#events.has(event)) return this;
const listeners = this.#events.get(event);
const index = listeners.findIndex(l =>
l.fn === listener || l.original === listener
);
if (index !== -1) {
listeners.splice(index, 1);
}
return this;
}
emit(event, ...args) {
if (!this.#events.has(event)) return false;
const listeners = [...this.#events.get(event)]; // Clone to avoid mutation issues
listeners.forEach(({ fn }) => fn.apply(this, args));
return true;
}
listenerCount(event) {
return this.#events.get(event)?.length || 0;
}
}
// Usage
const store = new EventEmitter();
store.on('change', (state) => {
console.log('State changed:', state);
});
store.once('init', () => {
console.log('Initialized (only fires once)');
});
store.emit('init');
store.emit('change', { user: 'Alice' });
DOM Custom Events with Detail
// Create custom events with data payload
const cartEvent = new CustomEvent('cart:updated', {
detail: {
items: [
{ id: 1, name: 'Widget', qty: 2 },
{ id: 2, name: 'Gadget', qty: 1 }
],
total: 149.99,
timestamp: Date.now()
},
bubbles: true, // Allow event to bubble up
cancelable: true, // Allow preventDefault()
composed: true // Cross shadow DOM boundary
});
// Listen anywhere in the tree
document.addEventListener('cart:updated', (e) => {
console.log('Cart total:', e.detail.total);
console.log('Item count:', e.detail.items.length);
});
// Dispatch from any element
document.querySelector('#add-to-cart').addEventListener('click', () => {
document.dispatchEvent(cartEvent);
});
// Custom event with cancelable action
const beforeNavigate = new CustomEvent('navigation:before', {
detail: { url: '/new-page' },
cancelable: true
});
const proceed = element.dispatchEvent(beforeNavigate);
if (proceed) {
// No listener called preventDefault()
window.location.href = beforeNavigate.detail.url;
} else {
console.log('Navigation was cancelled');
}
Pub/Sub for Components
// Global PubSub for component communication
const PubSub = {
events: {},
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
const index = this.events[event].push(callback) - 1;
// Return unsubscribe function
return () => {
this.events[event].splice(index, 1);
};
},
publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(callback => callback(data));
}
};
// Component A: User selector
class UserSelector {
constructor() {
this.select = document.getElementById('user-select');
this.select.addEventListener('change', (e) => {
PubSub.publish('user:selected', {
userId: e.target.value
});
});
}
}
// Component B: User profile display
class UserProfile {
constructor() {
this.container = document.getElementById('profile');
this.unsubscribe = PubSub.subscribe('user:selected',
this.loadProfile.bind(this)
);
}
async loadProfile({ userId }) {
const user = await fetchUser(userId);
this.container.innerHTML = `${user.name}
`;
}
destroy() {
this.unsubscribe(); // Clean up subscription
}
}
Passive Event Listeners
Passive event listeners tell the browser that the handler won't call preventDefault(), allowing for smoother scrolling and better performance on touch devices.
Understanding Passive Listeners
// ❌ Without passive: browser must wait to see if preventDefault is called
document.addEventListener('touchmove', (e) => {
// Track touch position (no preventDefault needed)
console.log(e.touches[0].clientY);
}); // Browser waits, scroll may feel janky
// ✅ With passive: browser can scroll immediately
document.addEventListener('touchmove', (e) => {
console.log(e.touches[0].clientY);
}, { passive: true }); // Smooth scrolling guaranteed
// ⚠️ Attempting preventDefault in passive listener
document.addEventListener('wheel', (e) => {
e.preventDefault(); // Ignored! Console warning in dev tools
}, { passive: true });
// When you NEED to prevent default (e.g., custom scroll)
document.addEventListener('wheel', (e) => {
e.preventDefault();
// Custom scroll behavior
customScroll(e.deltaY);
}, { passive: false }); // Explicitly non-passive
Feature Detection
// Detect passive event listener support
let passiveSupported = false;
try {
const options = {
get passive() {
passiveSupported = true;
return true;
}
};
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
passiveSupported = false;
}
// Use with fallback
function addPassiveListener(element, event, handler) {
element.addEventListener(
event,
handler,
passiveSupported ? { passive: true } : false
);
}
// Common use case: smooth scroll tracking
addPassiveListener(window, 'scroll', () => {
const scrollPercent = (window.scrollY /
(document.body.scrollHeight - window.innerHeight)) * 100;
updateScrollIndicator(scrollPercent);
});
- scroll: Always use passive unless implementing custom scroll
- touchstart/touchmove: Use passive for tracking, non-passive for gestures that prevent scrolling
- wheel: Passive for scroll-linked effects, non-passive for zoom controls
- Note: Chrome makes touchstart/touchmove passive by default on document-level
Pointer Events Deep Dive
Pointer Events provide a unified API for handling mouse, touch, and stylus input. They simplify cross-device development and provide additional capabilities.
Pointer Event Types
const canvas = document.getElementById('drawing-canvas');
// Pointer events mirror mouse events but work everywhere
canvas.addEventListener('pointerdown', (e) => {
console.log('Pointer ID:', e.pointerId); // Unique per pointer
console.log('Pointer type:', e.pointerType); // "mouse", "touch", "pen"
console.log('Is primary:', e.isPrimary); // First finger/main mouse button
console.log('Width/Height:', e.width, e.height); // Contact geometry
console.log('Pressure:', e.pressure); // 0-1 (stylus pressure)
console.log('Tilt:', e.tiltX, e.tiltY); // Stylus angle
});
// Capture pointer for reliable drag operations
canvas.addEventListener('pointerdown', (e) => {
canvas.setPointerCapture(e.pointerId);
// Now pointermove/up events come to this element even if pointer leaves
});
canvas.addEventListener('pointerup', (e) => {
canvas.releasePointerCapture(e.pointerId);
});
// Detect when capture is acquired/lost
canvas.addEventListener('gotpointercapture', (e) => {
console.log('Captured pointer:', e.pointerId);
});
canvas.addEventListener('lostpointercapture', (e) => {
console.log('Released pointer:', e.pointerId);
});
// Cancel event (touch cancelled, pen left detection range)
canvas.addEventListener('pointercancel', (e) => {
console.log('Pointer cancelled:', e.pointerId);
// Clean up any in-progress operations
});
Multi-touch with Pointer Events
// Track multiple simultaneous pointers
class MultiTouchHandler {
constructor(element) {
this.element = element;
this.pointers = new Map(); // pointerId -> pointer data
element.addEventListener('pointerdown', this.onDown.bind(this));
element.addEventListener('pointermove', this.onMove.bind(this));
element.addEventListener('pointerup', this.onUp.bind(this));
element.addEventListener('pointercancel', this.onUp.bind(this));
// Prevent default touch behaviors
element.style.touchAction = 'none';
}
onDown(e) {
this.element.setPointerCapture(e.pointerId);
this.pointers.set(e.pointerId, {
id: e.pointerId,
type: e.pointerType,
startX: e.clientX,
startY: e.clientY,
currentX: e.clientX,
currentY: e.clientY
});
this.updateGesture();
}
onMove(e) {
if (!this.pointers.has(e.pointerId)) return;
const pointer = this.pointers.get(e.pointerId);
pointer.currentX = e.clientX;
pointer.currentY = e.clientY;
this.updateGesture();
}
onUp(e) {
this.pointers.delete(e.pointerId);
this.updateGesture();
}
updateGesture() {
const count = this.pointers.size;
if (count === 2) {
// Pinch/zoom gesture
const [p1, p2] = [...this.pointers.values()];
const distance = Math.hypot(
p2.currentX - p1.currentX,
p2.currentY - p1.currentY
);
this.onPinch?.(distance);
} else if (count === 1) {
// Single pointer drag
const [pointer] = [...this.pointers.values()];
const dx = pointer.currentX - pointer.startX;
const dy = pointer.currentY - pointer.startY;
this.onDrag?.(dx, dy);
}
}
}
// Usage
const handler = new MultiTouchHandler(document.getElementById('zoom-area'));
handler.onPinch = (distance) => console.log('Pinch distance:', distance);
handler.onDrag = (dx, dy) => console.log('Drag:', dx, dy);
CSS touch-action
// CSS touch-action controls which gestures the browser handles
// Set via CSS or JavaScript
// Allow no browser gestures (handle everything in JS)
element.style.touchAction = 'none';
// Allow only horizontal panning
element.style.touchAction = 'pan-x';
// Allow only vertical panning
element.style.touchAction = 'pan-y';
// Allow panning but no pinch-zoom
element.style.touchAction = 'pan-x pan-y';
// Allow pinch-zoom only
element.style.touchAction = 'pinch-zoom';
// Common patterns
const carousel = document.querySelector('.carousel');
carousel.style.touchAction = 'pan-y'; // Allow vertical scroll, handle horizontal
const imageViewer = document.querySelector('.image-viewer');
imageViewer.style.touchAction = 'none'; // Handle all gestures for pan/zoom
const scrollableList = document.querySelector('.list');
scrollableList.style.touchAction = 'pan-y pinch-zoom'; // Standard scroll + zoom
Debouncing and Throttling Events
High-frequency events like scroll, resize, and input can fire hundreds of times per second. Debouncing and throttling control how often your handlers execute.
Debounce Implementation
// Debounce: wait until activity stops, then execute once
function debounce(fn, delay, options = {}) {
let timeoutId;
let lastArgs;
const { leading = false, trailing = true } = options;
function debounced(...args) {
lastArgs = args;
const shouldCallNow = leading && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (trailing && lastArgs) {
fn.apply(this, lastArgs);
lastArgs = null;
}
}, delay);
if (shouldCallNow) {
fn.apply(this, args);
}
}
debounced.cancel = () => {
clearTimeout(timeoutId);
timeoutId = null;
lastArgs = null;
};
return debounced;
}
// Usage: Search input (waits until user stops typing)
const searchInput = document.getElementById('search');
const handleSearch = debounce(async (e) => {
const results = await searchAPI(e.target.value);
displayResults(results);
}, 300);
searchInput.addEventListener('input', handleSearch);
// Leading edge: execute immediately, ignore subsequent calls
const saveDebounced = debounce(save, 1000, { leading: true, trailing: false });
// Both edges: execute on first and last call
const logDebounced = debounce(log, 1000, { leading: true, trailing: true });
Throttle Implementation
// Throttle: execute at most once per interval
function throttle(fn, limit, options = {}) {
let lastCall = 0;
let timeoutId;
let lastArgs;
const { leading = true, trailing = true } = options;
function throttled(...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCall;
lastArgs = args;
if (!lastCall && !leading) {
lastCall = now;
}
if (timeSinceLastCall >= limit) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastCall = now;
fn.apply(this, args);
} else if (!timeoutId && trailing) {
timeoutId = setTimeout(() => {
lastCall = leading ? Date.now() : 0;
timeoutId = null;
fn.apply(this, lastArgs);
}, limit - timeSinceLastCall);
}
}
throttled.cancel = () => {
clearTimeout(timeoutId);
timeoutId = null;
lastCall = 0;
};
return throttled;
}
// Usage: Scroll handler (executes at most every 100ms)
const handleScroll = throttle(() => {
const scrollPercent = window.scrollY /
(document.body.scrollHeight - window.innerHeight);
updateProgressBar(scrollPercent);
}, 100);
window.addEventListener('scroll', handleScroll, { passive: true });
// Resize handler
const handleResize = throttle(() => {
recalculateLayout();
}, 200);
window.addEventListener('resize', handleResize);
RequestAnimationFrame Throttle
// Throttle to animation frames (optimal for visual updates)
function rafThrottle(fn) {
let rafId = null;
let lastArgs = null;
function throttled(...args) {
lastArgs = args;
if (rafId === null) {
rafId = requestAnimationFrame(() => {
fn.apply(this, lastArgs);
rafId = null;
});
}
}
throttled.cancel = () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
return throttled;
}
// Perfect for visual scroll effects
const updateParallax = rafThrottle((scrollY) => {
const layers = document.querySelectorAll('.parallax-layer');
layers.forEach((layer, i) => {
const speed = (i + 1) * 0.1;
layer.style.transform = `translateY(${scrollY * speed}px)`;
});
});
window.addEventListener('scroll', () => {
updateParallax(window.scrollY);
}, { passive: true });
// Mouse move effects
const spotlight = document.querySelector('.spotlight');
const updateSpotlight = rafThrottle((x, y) => {
spotlight.style.transform = `translate(${x}px, ${y}px)`;
});
document.addEventListener('mousemove', (e) => {
updateSpotlight(e.clientX, e.clientY);
});
- Debounce: Use when you only care about the final state (search input, resize end, save draft)
- Throttle: Use when you need regular updates (scroll progress, drag position, analytics)
- RAF throttle: Use for visual updates synced to screen refresh rate
Summary
Mouse Events
click, dblclick, mouseenter/leave, mousemove
Keyboard Events
keydown, keyup, modifier keys, shortcuts
Form Events
submit, input, change, focus, blur
Propagation
Capturing → Target → Bubbling
Custom Events
CustomEvent with detail, dispatchEvent
Window Events
load, resize, scroll, visibility