Event Handling (addEventListener, removeEventListener)
1. Event Listeners (addEventListener, removeEventListener, once)
| Method | Syntax | Description | Browser Support |
|---|---|---|---|
| addEventListener | target.addEventListener(type, listener, options) |
Registers event listener on target. Supports multiple listeners per event. Third parameter is options object or boolean (useCapture). | All Browsers |
| removeEventListener | target.removeEventListener(type, listener, options) |
Removes event listener. Must match exact function reference and options used in addEventListener. | All Browsers |
| dispatchEvent | target.dispatchEvent(event) |
Dispatches event to target. Returns false if event is cancelable and any handler called preventDefault(). |
All Browsers |
| Option | Type | Description | Default |
|---|---|---|---|
| capture | boolean |
If true, listener fires during capture phase before target phase |
false |
| once NEW | boolean |
If true, listener automatically removed after first invocation |
false |
| passive NEW | boolean |
If true, listener will never call preventDefault(). Improves scroll performance. |
false |
| signal NEW | AbortSignal |
AbortSignal to remove listener when signal is aborted. Modern alternative to removeEventListener. | undefined |
Example: Event listener registration patterns
// Basic event listener
button.addEventListener("click", (event) => {
console.log("Button clicked!", event);
});
// Options object - once option
button.addEventListener("click", handleClick, { "once": true });
// Automatically removed after first click
// Passive listener for scroll performance
document.addEventListener("scroll", handleScroll, { "passive": true });
// Cannot call event.preventDefault()
// Capture phase
parent.addEventListener("click", handleCapture, { "capture": true });
// Fires before child's event
// Remove event listener
const handler = (e) => console.log(e);
button.addEventListener("click", handler);
button.removeEventListener("click", handler); // Must use same function reference
// AbortController for easy removal
const controller = new AbortController();
button.addEventListener("click", handleClick, { "signal": controller.signal });
button2.addEventListener("click", handleClick2, { "signal": controller.signal });
// Remove all listeners at once
controller.abort();
Note: Use
passive: true for scroll and touch events to improve performance - browser won't wait for listener to finish before scrolling. The once option is perfect for one-time events like splash screens or initial setup. AbortController provides a clean way to remove multiple listeners.
2. Event Object (preventDefault, stopPropagation, target)
| Property/Method | Type | Description | Use Case |
|---|---|---|---|
| type | string |
Event type (e.g., "click", "keydown"). Read-only. | Identify event type |
| target | EventTarget |
Element that originated event. Remains same through bubbling. Read-only. | Get original target |
| currentTarget | EventTarget |
Element with listener attached. Changes during bubbling/capture. Read-only. | Get current handler element |
| eventPhase | number |
Current phase: 0=NONE, 1=CAPTURING, 2=AT_TARGET, 3=BUBBLING. Read-only. | Determine event phase |
| bubbles | boolean |
Indicates if event bubbles up DOM tree. Read-only. | Check if event bubbles |
| cancelable | boolean |
Indicates if event can be canceled. Read-only. | Check if cancelable |
| defaultPrevented | boolean |
True if preventDefault() was called. Read-only. | Check if default prevented |
| timeStamp | number |
Time event was created (milliseconds since page load). Read-only. | Measure timing |
| isTrusted | boolean |
True if event initiated by user action, false if created by script. Read-only. | Verify user interaction |
| preventDefault() | void |
Cancels default browser action if event is cancelable. Does nothing if not cancelable. | Prevent default behavior |
| stopPropagation() | void |
Stops event from bubbling/capturing to other elements. Current element's listeners still fire. | Stop event propagation |
| stopImmediatePropagation() | void |
Stops propagation and prevents other listeners on current element from firing. | Stop all propagation |
| composedPath() | EventTarget[] |
Returns array of elements event will/did traverse. Includes shadow DOM elements. | Get event path |
Example: Event object properties
document.addEventListener("click", (event) => {
// Event identification
console.log("Type:", event.type); // "click"
console.log("Target:", event.target); // Element clicked
console.log("Current:", event.currentTarget); // document
// Event characteristics
console.log("Bubbles:", event.bubbles); // true
console.log("Cancelable:", event.cancelable); // true
console.log("Trusted:", event.isTrusted); // true (user action)
console.log("Time:", event.timeStamp); // 1234.567
// Event path
const path = event.composedPath();
console.log("Path:", path); // [target, parent, ..., window]
// Check phase
if (event.eventPhase === Event.AT_TARGET) {
console.log("At target element");
}
});
Example: Controlling event behavior
// Prevent default link navigation
link.addEventListener("click", (event) => {
event.preventDefault();
console.log("Navigation prevented");
});
// Stop event bubbling
child.addEventListener("click", (event) => {
event.stopPropagation();
console.log("Won't bubble to parent");
});
// Stop all propagation
element.addEventListener("click", (event) => {
event.stopImmediatePropagation();
console.log("No other listeners will fire");
});
// Check if default was prevented
form.addEventListener("submit", (event) => {
if (!event.defaultPrevented) {
console.log("Form will submit");
}
});
Warning:
stopPropagation() prevents parent handlers from executing, which can break event delegation patterns. Use with caution. event.target is the original element while event.currentTarget changes during propagation - common source of bugs.
3. Custom Events (CustomEvent, dispatchEvent)
| Constructor | Syntax | Description | Use Case |
|---|---|---|---|
| Event | new Event(type, options) |
Creates basic event. Options: bubbles, cancelable, composed. | Simple custom events |
| CustomEvent | new CustomEvent(type, options) |
Creates event with custom data. Adds detail property to options. |
Events with data payload |
| MouseEvent | new MouseEvent(type, options) |
Creates mouse event with coordinates, buttons, modifiers. Includes clientX, clientY, button, etc. | Simulate mouse events |
| KeyboardEvent | new KeyboardEvent(type, options) |
Creates keyboard event. Options: key, code, keyCode, ctrlKey, shiftKey, altKey, metaKey. | Simulate keyboard events |
| FocusEvent | new FocusEvent(type, options) |
Creates focus event. Includes relatedTarget for element losing/gaining focus. | Simulate focus events |
| InputEvent | new InputEvent(type, options) |
Creates input event. Options: data, inputType, isComposing. For text input simulation. | Simulate text input |
| Event Option | Type | Description | Default |
|---|---|---|---|
| bubbles | boolean |
Whether event bubbles up DOM tree | false |
| cancelable | boolean |
Whether event can be canceled with preventDefault() | false |
| composed | boolean |
Whether event propagates across shadow DOM boundary | false |
| detail | any |
Custom data for CustomEvent. Accessible via event.detail. | null |
Example: Creating and dispatching custom events
// Simple custom event
const simpleEvent = new Event("userLogin", {
"bubbles": true,
"cancelable": true
});
element.dispatchEvent(simpleEvent);
// CustomEvent with data payload
const customEvent = new CustomEvent("dataUpdate", {
"bubbles": true,
"detail": {
"userId": 123,
"timestamp": Date.now(),
"changes": ["name", "email"]
}
});
element.dispatchEvent(customEvent);
// Listen for custom event
element.addEventListener("dataUpdate", (event) => {
console.log("User ID:", event.detail.userId);
console.log("Changes:", event.detail.changes);
});
// Dispatch with return value check
const event = new CustomEvent("beforeSave", {
"cancelable": true,
"detail": { "data": formData }
});
const allowed = element.dispatchEvent(event);
if (!allowed) {
console.log("Save was prevented by listener");
}
Example: Simulating native events
// Simulate mouse click
const clickEvent = new MouseEvent("click", {
"bubbles": true,
"cancelable": true,
"clientX": 100,
"clientY": 200,
"button": 0 // Left button
});
button.dispatchEvent(clickEvent);
// Simulate keyboard event
const keyEvent = new KeyboardEvent("keydown", {
"key": "Enter",
"code": "Enter",
"keyCode": 13,
"bubbles": true,
"cancelable": true,
"ctrlKey": false,
"shiftKey": false
});
input.dispatchEvent(keyEvent);
// Simulate input event
const inputEvent = new InputEvent("input", {
"bubbles": true,
"data": "text",
"inputType": "insertText"
});
textarea.dispatchEvent(inputEvent);
Note: Use
CustomEvent instead of Event when you need to pass data - the detail property is the standard way to attach custom data. Always set bubbles: true unless you specifically want to prevent event propagation. For component communication, custom events are cleaner than callbacks.
4. Touch and Pointer Events (touchstart, pointerdown)
| Event Type | Description | Fires When | Browser Support |
|---|---|---|---|
| pointerdown | Pointer becomes active (mouse, touch, pen) | Button pressed, screen touched, pen contact | Modern Browsers |
| pointerup | Pointer becomes inactive | Button released, touch ended, pen lifted | Modern Browsers |
| pointermove | Pointer position changes | Mouse/touch/pen moves | Modern Browsers |
| pointercancel | Pointer event canceled by browser | Touch interrupted, too many touches | Modern Browsers |
| pointerenter | Pointer enters element boundary | Cursor enters, touch starts in element. Doesn't bubble. | Modern Browsers |
| pointerleave | Pointer leaves element boundary | Cursor leaves, touch ends outside. Doesn't bubble. | Modern Browsers |
| touchstart | Touch point placed on surface | Finger touches screen | Mobile Browsers |
| touchmove | Touch point moves along surface | Finger drags on screen | Mobile Browsers |
| touchend | Touch point removed from surface | Finger lifted from screen | Mobile Browsers |
| touchcancel | Touch point disrupted | Touch interrupted by system (call, alert) | Mobile Browsers |
| PointerEvent Property | Type | Description |
|---|---|---|
| pointerId | number |
Unique identifier for pointer. Same pointer keeps same ID through interaction. |
| pointerType | string |
Type of pointer: "mouse", "touch", "pen" |
| isPrimary | boolean |
True if primary pointer of type (first touch, main mouse) |
| pressure | number |
Pressure normalized to 0-1. 0.5 for devices without pressure. |
| tiltX / tiltY | number |
Angle of pen/stylus in degrees (-90 to 90) |
| width / height | number |
Contact geometry in CSS pixels |
| clientX / clientY | number |
Coordinates relative to viewport |
| pageX / pageY | number |
Coordinates relative to document |
| screenX / screenY | number |
Coordinates relative to screen |
| TouchEvent Property | Type | Description |
|---|---|---|
| touches | TouchList |
All current touches on surface |
| targetTouches | TouchList |
Touches on current target element |
| changedTouches | TouchList |
Touches that triggered this event |
Example: Pointer events for unified input handling
// Unified pointer handling (mouse, touch, pen)
element.addEventListener("pointerdown", (event) => {
console.log("Pointer type:", event.pointerType); // "mouse", "touch", "pen"
console.log("Pointer ID:", event.pointerId);
console.log("Primary:", event.isPrimary);
console.log("Pressure:", event.pressure);
console.log("Position:", event.clientX, event.clientY);
// Capture pointer for drag operation
element.setPointerCapture(event.pointerId);
});
element.addEventListener("pointermove", (event) => {
if (event.pointerType === "touch") {
// Handle touch-specific behavior
}
});
element.addEventListener("pointerup", (event) => {
element.releasePointerCapture(event.pointerId);
});
// Prevent touch scrolling while drawing
canvas.addEventListener("touchstart", (event) => {
event.preventDefault();
}, { "passive": false });
Example: Multi-touch handling
// Track multiple simultaneous touches
element.addEventListener("touchstart", (event) => {
console.log("Active touches:", event.touches.length);
console.log("Target touches:", event.targetTouches.length);
console.log("Changed touches:", event.changedTouches.length);
// Access individual touches
for (let touch of event.touches) {
console.log("Touch ID:", touch.identifier);
console.log("Position:", touch.clientX, touch.clientY);
}
});
// Pinch-to-zoom detection
let initialDistance = 0;
element.addEventListener("touchstart", (event) => {
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
initialDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
}
});
element.addEventListener("touchmove", (event) => {
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
const scale = distance / initialDistance;
console.log("Zoom scale:", scale);
}
});
Note: Prefer Pointer Events over touch/mouse events - they unify all input types. Use
setPointerCapture() to ensure pointer events continue firing even if pointer moves outside element during drag. Set touch-action CSS property to control touch behaviors.
5. Keyboard and Mouse Events (keydown, click, mouseover)
| Event Type | Description | Fires When | Cancelable |
|---|---|---|---|
| keydown | Key pressed down | Key initially pressed. Repeats while held. | Yes |
| keyup | Key released | Key released after press | Yes |
| keypress DEPRECATED | Character key pressed (deprecated) | Use keydown instead | Yes |
| click | Element clicked | Mouse down + up on same element | Yes |
| dblclick | Element double-clicked | Two clicks in quick succession | Yes |
| mousedown | Mouse button pressed | Button pressed on element | Yes |
| mouseup | Mouse button released | Button released | Yes |
| mousemove | Mouse pointer moved | Pointer moves over element | Yes |
| mouseenter | Mouse enters element | Pointer enters boundary. Doesn't bubble. | No |
| mouseleave | Mouse leaves element | Pointer leaves boundary. Doesn't bubble. | No |
| mouseover | Mouse enters element or child | Pointer enters. Bubbles. | Yes |
| mouseout | Mouse leaves element or child | Pointer leaves. Bubbles. | Yes |
| contextmenu | Context menu triggered | Right-click or long-press | Yes |
| wheel | Mouse wheel scrolled | Wheel rotated over element | Yes |
| KeyboardEvent Property | Type | Description | Example Values |
|---|---|---|---|
| key | string |
Value of key pressed. Preferred modern property. | "a", "Enter", "ArrowLeft" |
| code | string |
Physical key code. Independent of keyboard layout. | "KeyA", "Enter", "ArrowLeft" |
| keyCode DEPRECATED | number |
Numeric key code (deprecated). Use key or code instead. |
65, 13, 37 |
| ctrlKey | boolean |
True if Ctrl/Command key pressed during event | true / false |
| shiftKey | boolean |
True if Shift key pressed during event | true / false |
| altKey | boolean |
True if Alt/Option key pressed during event | true / false |
| metaKey | boolean |
True if Meta key pressed (Windows/Command key) | true / false |
| repeat | boolean |
True if key held down (auto-repeat) | true / false |
| MouseEvent Property | Type | Description | Values |
|---|---|---|---|
| button | number |
Which button was pressed | 0=Left, 1=Middle, 2=Right, 3=Back, 4=Forward |
| buttons | number |
Bitmask of currently pressed buttons | 1=Left, 2=Right, 4=Middle (can combine) |
| clientX / clientY | number |
Coordinates relative to viewport | Pixel values |
| pageX / pageY | number |
Coordinates relative to document | Pixel values |
| screenX / screenY | number |
Coordinates relative to screen | Pixel values |
| offsetX / offsetY | number |
Coordinates relative to target element | Pixel values |
| movementX / movementY | number |
Distance moved since last mousemove | Delta in pixels |
| relatedTarget | Element |
Secondary target (for mouseenter/mouseleave) | Element reference |
Example: Keyboard event handling
// Modern key detection
document.addEventListener("keydown", (event) => {
// Use 'key' property (preferred)
if (event.key === "Enter") {
console.log("Enter pressed");
}
// Use 'code' for physical key position
if (event.code === "KeyA") {
console.log("A key pressed (QWERTY position)");
}
// Modifier keys
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
console.log("Ctrl+S (Save)");
}
// Check for key combinations
if (event.shiftKey && event.altKey && event.key === "K") {
console.log("Shift+Alt+K pressed");
}
// Arrow keys
switch(event.key) {
case "ArrowUp": moveUp(); break;
case "ArrowDown": moveDown(); break;
case "ArrowLeft": moveLeft(); break;
case "ArrowRight": moveRight(); break;
}
// Prevent auto-repeat
if (event.repeat) return;
});
Example: Mouse event handling
// Mouse button detection
element.addEventListener("mousedown", (event) => {
switch(event.button) {
case 0: console.log("Left click"); break;
case 1: console.log("Middle click"); break;
case 2: console.log("Right click"); break;
}
});
// Mouse position
element.addEventListener("click", (event) => {
console.log("Viewport:", event.clientX, event.clientY);
console.log("Document:", event.pageX, event.pageY);
console.log("Element:", event.offsetX, event.offsetY);
console.log("Screen:", event.screenX, event.screenY);
});
// Drag detection
let isDragging = false;
element.addEventListener("mousedown", () => {
isDragging = true;
});
document.addEventListener("mousemove", (event) => {
if (isDragging) {
console.log("Dragging:", event.movementX, event.movementY);
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
});
// Prevent context menu
element.addEventListener("contextmenu", (event) => {
event.preventDefault();
console.log("Custom context menu");
});
Note: Always use
event.key instead of deprecated keyCode. Use mouseenter/mouseleave instead of mouseover/mouseout when you don't want events from child elements. For keyboard shortcuts, check modifier keys with ctrlKey, shiftKey, altKey, metaKey.
6. Event Delegation and Bubbling
| Concept | Description | Benefits | Use Case |
|---|---|---|---|
| Event Bubbling | Events propagate from target element up through ancestors to document root | Enables event delegation. Single listener handles multiple elements. | Default event flow in DOM |
| Event Capturing | Events propagate from document root down to target element (before bubbling) | Intercept events before target. Use capture: true option. |
Special interception needs |
| Event Delegation | Single listener on parent handles events from multiple children via bubbling | Better performance. Works with dynamic content. Less memory. | Lists, tables, dynamic UI |
| Event Target | event.target is element that triggered event (child) |
Identify actual clicked element in delegation pattern | Determine source element |
| Current Target | event.currentTarget is element with listener attached (parent) |
Reference to delegating element | Access parent in delegation |
| Event Phase | Value | Description | Direction |
|---|---|---|---|
| NONE | 0 |
Event not being processed | N/A |
| CAPTURING_PHASE | 1 |
Event traveling from root to target | Top-down |
| AT_TARGET | 2 |
Event reached target element | At target |
| BUBBLING_PHASE | 3 |
Event traveling from target to root | Bottom-up |
Example: Event delegation pattern
// Instead of attaching listener to each item
// ❌ Bad: Many listeners, doesn't work with dynamic content
document.querySelectorAll(".item").forEach((item) => {
item.addEventListener("click", handleClick);
});
// ✅ Good: Single listener on parent
const list = document.querySelector("#list");
list.addEventListener("click", (event) => {
// Check if clicked element matches selector
if (event.target.matches(".item")) {
console.log("Item clicked:", event.target);
handleItem(event.target);
}
// Or use closest for nested elements
const item = event.target.closest(".item");
if (item && list.contains(item)) {
console.log("Item found:", item);
}
});
// Works automatically with dynamically added items
const newItem = document.createElement("div");
newItem.className = "item";
list.appendChild(newItem); // Click handler works immediately!
Example: Event propagation and phases
// Event flow: Capture → Target → Bubble
const parent = document.querySelector("#parent");
const child = document.querySelector("#child");
// Capture phase (top-down)
parent.addEventListener("click", (event) => {
console.log("Parent - Capture phase");
console.log("Phase:", event.eventPhase); // 1 (CAPTURING_PHASE)
}, { "capture": true });
// Target phase
child.addEventListener("click", (event) => {
console.log("Child - Target phase");
console.log("Phase:", event.eventPhase); // 2 (AT_TARGET)
console.log("Target:", event.target); // child
console.log("Current:", event.currentTarget); // child
});
// Bubble phase (bottom-up)
parent.addEventListener("click", (event) => {
console.log("Parent - Bubble phase");
console.log("Phase:", event.eventPhase); // 3 (BUBBLING_PHASE)
console.log("Target:", event.target); // child (original)
console.log("Current:", event.currentTarget); // parent
});
// Click child outputs:
// "Parent - Capture phase"
// "Child - Target phase"
// "Parent - Bubble phase"
Example: Advanced delegation patterns
// Multi-action delegation
document.querySelector("#toolbar").addEventListener("click", (event) => {
const button = event.target.closest("button");
if (!button) return;
const action = button.dataset.action;
switch(action) {
case "save":
handleSave();
break;
case "delete":
handleDelete();
break;
case "edit":
handleEdit();
break;
}
});
// Table row delegation
table.addEventListener("click", (event) => {
const row = event.target.closest("tr");
if (!row || row.parentElement.tagName === "THEAD") return;
const cell = event.target.closest("td");
const columnIndex = Array.from(row.cells).indexOf(cell);
console.log("Row:", row.rowIndex);
console.log("Column:", columnIndex);
console.log("Data:", row.dataset.id);
});
// Form delegation
form.addEventListener("input", (event) => {
const input = event.target;
if (input.matches("[data-validate]")) {
validateField(input);
}
});
// Performance: Delegate to document for global handlers
document.addEventListener("click", (event) => {
// Close dropdowns when clicking outside
if (!event.target.closest(".dropdown")) {
closeAllDropdowns();
}
});
Note: Event delegation is essential for performance with large lists (hundreds/thousands of items). It automatically handles dynamically added elements without re-attaching listeners. Use
event.target.closest(selector) to handle clicks on nested elements within delegated items.
Warning: Not all events bubble! Events like
focus, blur, mouseenter, mouseleave don't bubble. Use their bubbling alternatives: focusin, focusout, mouseover, mouseout for delegation. Always check if event.target matches your selector before acting.
Event Handling Best Practices
- Use
addEventListenerwith options object for modern features (once, passive, signal) - Prefer Pointer Events over separate mouse/touch handlers for unified input
- Use event delegation for lists and dynamic content - better performance and automatic handling of new elements
- Set
passive: truefor scroll/touch events to improve performance - Use
event.keyinstead of deprecatedkeyCodefor keyboard events - Remember:
event.target= original element,event.currentTarget= element with listener - Call
preventDefault()to stop default browser actions,stopPropagation()to prevent bubbling - Use
CustomEventwithdetailproperty for component communication - Clean up listeners with
AbortControllerorremoveEventListenerto prevent memory leaks