|$ curl https://forge-ai.dev/api/markdown?path=docs/js/events
$cat docs/javascript-events.md
updated This week·35 min read·published

JavaScript Events

JavaScriptEventsIntermediateIntermediate
Introduction

Event-driven programming is the backbone of interactive web applications. The browser continuously emits events — clicks, key presses, scrolls, form submissions, touch gestures — and your JavaScript code responds to them. Understanding the event system is essential for building responsive, performant interfaces.

At its core, an event is a signal that something has happened. The browser creates an Event object with information about the action — which element was clicked, which key was pressed, the mouse coordinates, or the touch points. Your code registers listeners (also called handlers) that execute when the event occurs.

The DOM Events specification defines three phases of event propagation: capturing, target, and bubbling. Understanding this flow is critical for debugging and controlling event behavior, especially in complex UIs with nested elements.

Event Types

The browser supports dozens of event types organized by category. Each event type has a specific interface — mouse events include coordinates and button info, keyboard events include key codes and modifier states, and touch events include touch points and gestures.

CategoryEvent TypesInterface
Mouseclick, dblclick, mousedown, mouseup, mousemove, mouseover, mouseout, mouseenter, mouseleave, contextmenuMouseEvent
Keyboardkeydown, keyup, keypress (deprecated)KeyboardEvent
Formsubmit, reset, change, input, focus, blur, focusin, focusout, select, invalidEvent / FocusEvent
Focusfocus, blur, focusin, focusoutFocusEvent
Scroll / Resizescroll, resize, wheelEvent / WheelEvent
Touchtouchstart, touchmove, touchend, touchcancelTouchEvent
Window / Documentload, DOMContentLoaded, beforeunload, unload, hashchange, popstate, resize, online, offlineEvent / HashChangeEvent / PopStateEvent
Clipboardcopy, cut, pasteClipboardEvent
Drag & Dropdrag, dragstart, dragend, dragenter, dragover, dragleave, dropDragEvent
Animationanimationstart, animationend, animationiteration, transitionstart, transitionend, transitioncancelAnimationEvent / TransitionEvent
Mediaplay, pause, ended, timeupdate, volumechange, loadedmetadata, canplay, waiting, seeking, progress, errorEvent
event-types.js
JavaScript
1// Common event type examples
2document.addEventListener('click', (e) => {
3 console.log('Clicked at:', e.clientX, e.clientY);
4});
5
6document.addEventListener('keydown', (e) => {
7 if (e.key === 'Escape') {
8 console.log('Modal dismissed via keyboard');
9 }
10});
11
12window.addEventListener('resize', () => {
13 console.log('Viewport:', window.innerWidth, 'x', window.innerHeight);
14});
15
16element.addEventListener('touchstart', (e) => {
17 const touch = e.touches[0];
18 console.log('Touch at:', touch.clientX, touch.clientY);
19});
addEventListener

The addEventListener method is the modern, standards-compliant way to register event handlers. It replaces the older onclick / onchange attribute approach and the legacy attachEvent (Internet Explorer only). Unlike inline handlers, addEventListener allows multiple listeners for the same event on the same element and gives fine-grained control over capture phase, passive mode, and one-time execution.

addEventListener.js
JavaScript
1// Modern approach — addEventListener
2const button = document.getElementById('submitBtn');
3
4button.addEventListener('click', function handleClick(e) {
5 console.log('Button clicked');
6});
7
8button.addEventListener('click', function anotherHandler(e) {
9 console.log('This also runs — multiple listeners');
10});
11
12// Options object
13button.addEventListener('click', handler, {
14 capture: false, // listen during bubbling phase (default)
15 once: true, // auto-remove after first invocation
16 passive: true, // signal that preventDefault will not be called
17});
18
19// Once option — fires once then removes itself
20button.addEventListener('click', () => {
21 console.log('This runs only once');
22}, { once: true });
23
24// Remove listener (requires named function reference)
25button.removeEventListener('click', handleClick);

The options object supports three boolean flags. capture switches to the capturing phase. once ensures the listener fires at most once and then removes itself — useful for initialization events. passive tells the browser the listener will never call preventDefault(), enabling scroll optimizations.

MethodMultiple ListenersRemove ListenerCapture PhaseBrowser Support
addEventListenerremoveEventListener✓ (third arg)All modern browsers
onclick attribute✗ (overwrites)Set to nullLegacy, avoid
attachEventdetachEventIE only (obsolete)

best practice

Always use addEventListener over inline event handlers like onclick="...". Inline handlers create global function dependencies, mix behavior with markup, and cannot register multiple handlers. Use named functions (not anonymous closures) when you need to call removeEventListener later.
The Event Object

Every event handler receives an Event object as its first argument. This object contains properties and methods that describe the event and allow you to control its behavior. The specific interface depends on the event type — MouseEvent has clientX/clientY, KeyboardEvent has key/code, and TouchEvent has touches/changedTouches.

Property / MethodTypeDescription
targetElementThe element that dispatched the event (the innermost clicked element)
currentTargetElementThe element the listener is attached to (may differ from target during bubbling)
typestringEvent type name: click, keydown, submit, etc.
eventPhasenumber1 (capture), 2 (at target), 3 (bubbling)
bubblesbooleanWhether the event propagates upward through the DOM
cancelablebooleanWhether preventDefault is allowed
defaultPreventedbooleanWhether preventDefault was called
timestampnumberTime when the event was created (ms since page load)
preventDefault()methodCancels the default browser action (e.g., link navigation, form submission)
stopPropagation()methodStops further propagation (bubbling or capturing)
stopImmediatePropagation()methodStops propagation and prevents other listeners on the same element
event-object.js
JavaScript
1// Event object properties in action
2document.querySelector('a').addEventListener('click', (e) => {
3 console.log('Target:', e.target); // The <a> element
4 console.log('Current target:', e.currentTarget); // Same element (no bubbling here)
5 console.log('Type:', e.type); // "click"
6 console.log('Phase:', e.eventPhase); // 2 (at target)
7 console.log('Bubbles:', e.bubbles); // true
8 console.log('Cancelable:', e.cancelable); // true
9 console.log('Timestamp:', e.timeStamp); // 4823.4
10
11 e.preventDefault(); // Prevent navigation
12 console.log('Default prevented:', e.defaultPrevented); // true
13});
14
15// MouseEvent specific properties
16document.addEventListener('click', (e) => {
17 const { clientX, clientY, screenX, screenY, altKey, ctrlKey, shiftKey, button } = e;
18 console.log({ clientX, clientY, screenX, screenY, altKey, ctrlKey, shiftKey, button });
19});
20
21// KeyboardEvent specific properties
22document.addEventListener('keydown', (e) => {
23 const { key, code, repeat, altKey, ctrlKey, shiftKey, metaKey } = e;
24 console.log({ key, code, repeat, altKey, ctrlKey, shiftKey, metaKey });
25 if (key === 'Enter' && e.shiftKey) {
26 console.log('Shift+Enter pressed');
27 }
28});
Event Propagation

When an event fires on a DOM element, it does not stay on that element. It travels through the DOM tree in three phases: capturing, target, and bubbling. Understanding propagation is essential for controlling event behavior and building reliable interaction patterns.

Event Propagation Phases
CAPTURING PHASE (eventPhase = 1) window → document → html → body → parent → target Listeners with `capture: true` fire here Event moves DOWN the DOM treeTARGET PHASE (eventPhase = 2) Listeners on the actual target element fire Both capture and bubble listeners on the target fire in registration orderBUBBLING PHASE (eventPhase = 3) target → parent → body → html → document → window Default phase for most events Event moves UP the DOM tree
propagation.html
JavaScript
1// Propagation demonstration
2<div id="outer" style="padding:20px;background:#1A1A2E">
3 <div id="inner" style="padding:20px;background:#2A2A4E">
4 <button id="btn">Click Me</button>
5 </div>
6</div>
7
8<script>
9// Capturing phase (third argument: true)
10document.getElementById('outer').addEventListener('click', () => {
11 console.log('CAPTURE: outer');
12}, true);
13
14document.getElementById('inner').addEventListener('click', () => {
15 console.log('CAPTURE: inner');
16}, true);
17
18// Target phase
19document.getElementById('btn').addEventListener('click', () => {
20 console.log('TARGET: button');
21});
22
23// Bubbling phase (default)
24document.getElementById('inner').addEventListener('click', () => {
25 console.log('BUBBLE: inner');
26});
27
28document.getElementById('outer').addEventListener('click', () => {
29 console.log('BUBBLE: outer');
30});
31
32// Click on button logs:
33// CAPTURE: outer
34// CAPTURE: inner
35// TARGET: button
36// BUBBLE: inner
37// BUBBLE: outer
38</script>
stop-propagation.js
JavaScript
1// Stopping propagation
2document.getElementById('inner').addEventListener('click', (e) => {
3 e.stopPropagation();
4 // Event will NOT reach outer during bubbling
5 // Does NOT prevent other listeners on inner
6});
7
8document.getElementById('btn').addEventListener('click', (e) => {
9 e.stopImmediatePropagation();
10 // Stops ALL propagation AND other listeners on this element
11});

warning

Use stopPropagation sparingly. It can break expected event behavior in complex UIs — dropdowns not closing, click-away listeners failing, and third-party libraries malfunctioning. Prefer checking e.target to filter events rather than stopping propagation entirely.
Event Delegation

Event delegation leverages event bubbling to handle events efficiently. Instead of attaching a listener to every child element, you attach a single listener to a parent element and use e.target to determine which child was interacted with. This is especially powerful for dynamic lists, tables, and any collection of similar elements.

AspectIndividual ListenersEvent Delegation
MemoryOne listener per elementSingle listener on parent
Dynamic elementsMust re-attach after DOM changesWorks automatically
PerformanceDegrades with many elementsConstant — O(1)
Target filteringAutomatic per-elementRequires manual checks
When to useFew static elements, unique handlersLists, tables, dynamic content
event-delegation.js
JavaScript
1// BAD — individual listeners (don't do this for many elements)
2document.querySelectorAll('.list-item').forEach(item => {
3 item.addEventListener('click', () => {
4 console.log('Clicked:', item.textContent);
5 });
6});
7
8// GOOD — event delegation
9document.querySelector('.list-container').addEventListener('click', (e) => {
10 // Find the closest list item
11 const item = e.target.closest('.list-item');
12 if (!item) return; // Ignore clicks outside items
13
14 console.log('Clicked:', item.textContent);
15 console.log('Data attr:', item.dataset.id);
16});
17
18// Pattern for filtering by data attribute
19document.querySelector('.toolbar').addEventListener('click', (e) => {
20 const button = e.target.closest('[data-action]');
21 if (!button) return;
22
23 const action = button.dataset.action;
24 switch (action) {
25 case 'save': saveDocument(); break;
26 case 'delete': deleteDocument(); break;
27 case 'export': exportDocument(); break;
28 }
29});
dynamic-delegation.html
JavaScript
1// Delegation with dynamic elements
2<ul id="todoList">
3 <li data-id="1" class="todo-item">Learn JavaScript <button class="delete">×</button></li>
4 <li data-id="2" class="todo-item">Build a project <button class="delete">×</button></li>
5</ul>
6<button id="addTodo">Add Todo</button>
7
8<script>
9const list = document.getElementById('todoList');
10
11// Single delegation listener handles ALL items including future ones
12list.addEventListener('click', (e) => {
13 // Delete button click
14 if (e.target.classList.contains('delete')) {
15 const item = e.target.closest('.todo-item');
16 item.remove();
17 console.log('Deleted item:', item.dataset.id);
18 return;
19 }
20
21 // Item click (but not delete button)
22 if (e.target.classList.contains('todo-item')) {
23 e.target.classList.toggle('completed');
24 }
25});
26
27// New items added dynamically still work with delegation
28document.getElementById('addTodo').addEventListener('click', () => {
29 const li = document.createElement('li');
30 li.className = 'todo-item';
31 li.dataset.id = Date.now();
32 li.innerHTML = 'New task <button class="delete">×</button>';
33 list.appendChild(li);
34});
35</script>
🔥

pro tip

Use element.closest() instead of e.target for delegation. closest() walks up from the target to find the matching ancestor, so it works even when the user clicks a child element (like a nested span or icon) inside the delegated item.
Custom Events

The browser is not the only source of events. You can create and dispatch your own custom events using the CustomEvent constructor. This enables a pub/sub communication pattern between decoupled components — one part of your application can signal state changes and other parts can listen without direct references.

custom-events.js
JavaScript
1// Creating and dispatching a custom event
2const event = new CustomEvent('userLogin', {
3 detail: {
4 userId: 42,
5 username: 'johndoe',
6 role: 'admin',
7 timestamp: Date.now()
8 },
9 bubbles: true,
10 cancelable: true
11});
12
13// Dispatch on any element
14document.getElementById('app').dispatchEvent(event);
15
16// Listen for the custom event
17document.addEventListener('userLogin', (e) => {
18 const { userId, username, role } = e.detail;
19 console.log(`User ${username} (${role}) logged in`);
20 updateUI(userId);
21});
22
23// Event-based component communication
24function showNotification(message, type = 'info') {
25 const event = new CustomEvent('notification', {
26 detail: { message, type },
27 bubbles: true
28 });
29 document.dispatchEvent(event);
30}
31
32document.addEventListener('notification', (e) => {
33 const { message, type } = e.detail;
34 renderToast(message, type);
35});
36
37// Using events for state management
38class Store extends EventTarget {
39 constructor(initial = {}) {
40 super();
41 this.state = initial;
42 }
43
44 setState(update) {
45 const prev = { ...this.state };
46 this.state = { ...this.state, ...update };
47 this.dispatchEvent(new CustomEvent('stateChange', {
48 detail: { state: this.state, prev }
49 }));
50 }
51
52 getState() {
53 return this.state;
54 }
55}
56
57const store = new Store({ count: 0 });
58store.addEventListener('stateChange', (e) => {
59 console.log('State changed:', e.detail.state);
60});
61store.setState({ count: 1 });

info

The detail property of CustomEvent can hold any data — objects, arrays, primitives. This is the standard way to pass data with custom events. Unlike DOM events, custom events are not bound to any specific element; you can dispatch them on any node.
Throttling & Debouncing

Some events fire at high frequency — scroll, resize, mousemove, and input events can fire hundreds of times per second. Running expensive computations on every event causes jank, battery drain, and poor UX. Throttling and debouncing limit how often a function executes, but they work differently.

StrategyBehaviorBest For
ThrottleExecutes at most once per interval, ensures regular executionScroll handlers, resize, progress tracking
DebounceExecutes after a period of inactivity, resets on each callSearch input, autocomplete, save drafts, resize end
throttle-debounce.js
JavaScript
1// Throttle implementation — runs at most once per interval
2function throttle(fn, delay = 100) {
3 let lastCall = 0;
4 let timer = null;
5
6 return function(...args) {
7 const now = Date.now();
8 const remaining = delay - (now - lastCall);
9
10 if (remaining <= 0) {
11 // Enough time has passed — execute immediately
12 if (timer) {
13 clearTimeout(timer);
14 timer = null;
15 }
16 lastCall = now;
17 fn.apply(this, args);
18 } else if (!timer) {
19 // Schedule execution at the end of remaining time
20 timer = setTimeout(() => {
21 lastCall = Date.now();
22 timer = null;
23 fn.apply(this, args);
24 }, remaining);
25 }
26 };
27}
28
29// Usage
30window.addEventListener('scroll', throttle(() => {
31 console.log('Scroll position:', window.scrollY);
32 updateProgressBar();
33}, 100));
34
35// Debounce implementation — runs after inactivity
36function debounce(fn, delay = 300) {
37 let timer = null;
38
39 return function(...args) {
40 clearTimeout(timer);
41 timer = setTimeout(() => {
42 timer = null;
43 fn.apply(this, args);
44 }, delay);
45 };
46}
47
48// Usage
49searchInput.addEventListener('input', debounce((e) => {
50 const query = e.target.value;
51 console.log('Searching:', query);
52 fetchSearchResults(query);
53}, 400));
54
55// Leading edge debounce — runs immediately, then waits
56function debounceLeading(fn, delay = 300) {
57 let timer = null;
58 let leading = true;
59
60 return function(...args) {
61 if (leading) {
62 leading = false;
63 fn.apply(this, args);
64 }
65 clearTimeout(timer);
66 timer = setTimeout(() => {
67 leading = true;
68 }, delay);
69 };
70}
71
72// Immediate save on first change, then debounce subsequent
73const saveHandler = debounceLeading(() => {
74 saveDocument();
75}, 2000);
preview

best practice

Add { passive: true } to scroll and touch event listeners when you do not need preventDefault(). This tells the browser it can optimize scrolling without waiting for your JavaScript. Touch events with passive: true can improve scroll performance significantly on mobile.
Best Practices
Always remove event listeners when the element is removed from the DOM to prevent memory leaks — use AbortController or cleanup in componentWillUnmount / useEffect return
Use passive: true for scroll, touchstart, touchmove, and wheel listeners when you do not need preventDefault — enables scroll optimizations
Never use inline event handlers (onclick="...") — they create global scope pollution, prevent multiple listeners, and mix concerns
Prefer event delegation for lists, tables, and collections of similar elements — fewer listeners means better memory and performance
Use e.target.closest() instead of e.target for delegation to handle nested child clicks correctly
Use named function references with addEventListener so you can remove them later with removeEventListener
Avoid stopPropagation unless absolutely necessary — it can break third-party libraries and expected event behavior in complex UIs
Throttle or debounce high-frequency events (scroll, resize, mousemove, input) to avoid jank and excessive computation
Use CustomEvent for component communication instead of tight coupling via direct function references or global state
Handle both keyboard and mouse/touch interactions — a button should work with click, Enter, and Space
event-cleanup.js
JavaScript
1// Cleanup with AbortController (modern pattern)
2const controller = new AbortController();
3const { signal } = controller;
4
5window.addEventListener('resize', handleResize, { signal });
6document.addEventListener('click', handleClick, { signal });
7element.addEventListener('scroll', handleScroll, { passive: true, signal });
8
9// Later — remove ALL listeners at once
10controller.abort();
11
12// React-style cleanup pattern
13function setupEventListeners(element) {
14 const ctrl = new AbortController();
15
16 element.addEventListener('click', handler, { signal: ctrl.signal });
17 window.addEventListener('resize', resizeHandler, { signal: ctrl.signal });
18
19 // Return cleanup function
20 return () => ctrl.abort();
21}
22
23const cleanup = setupEventListeners(myElement);
24// Later:
25cleanup(); // Removes both listeners
26
27// Keyboard and mouse together
28button.addEventListener('keydown', (e) => {
29 if (e.key === 'Enter' || e.key === ' ') {
30 e.preventDefault();
31 activate();
32 }
33});
34
35button.addEventListener('click', activate);

danger

Failing to remove event listeners from removed DOM elements causes memory leaks. The listener holds a reference to the element, preventing garbage collection. This is especially problematic in Single Page Applications where elements are frequently created and destroyed. Always pair addEventListener with a cleanup mechanism.
$Blueprint — Engineering Documentation·Section ID: JS-03·Revision: 1.0