Events Deep Dive

50 min read Intermediate

Master JavaScript events: mouse, keyboard, form events, event propagation, custom events, and advanced patterns for interactive applications.

Mouse Events

Handle various mouse interactions from clicks to movement.

JavaScript
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.

JavaScript
// 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");
    }
});
keypress is Deprecated

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.

JavaScript
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.

JavaScript
// 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
});
Bubbling vs Capturing

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.

JavaScript
// 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.

JavaScript
// 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.

JavaScript
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

JavaScript - Custom Event Emitter
// 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

JavaScript - CustomEvent with Data
// 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

JavaScript - PubSub Pattern
// 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

JavaScript - 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

JavaScript - Passive Support 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);
});
When to Use Passive Listeners
  • 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

JavaScript - Complete Pointer Events
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

JavaScript - Multi-Touch Handling
// 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

JavaScript - Touch Action Control
// 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

JavaScript - Debounce Function
// 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

JavaScript - Throttle Function
// 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

JavaScript - RAF-based Throttling
// 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 vs Throttle
  • 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