β‘ Event Handling
Events are actions or occurrences that happen in the browserβlike clicks, key presses, mouse movements, or page loads. Event handling allows you to respond to these user interactions and create dynamic, interactive web applications.
Event handling is the foundation of interactivity on the web. Every button click, form submission, and animation trigger relies on events.
π― What Are Events?
Events are signals that something has happened in the browser. They can be triggered by the user (clicks, typing) or by the browser itself (page load, timer completion).
Common Event Types
| Category | Events | Description |
|---|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave | Mouse interactions |
| Keyboard | keydown, keyup, keypress | Keyboard input |
| Form | submit, input, change, focus, blur | Form interactions |
| Document | DOMContentLoaded, load, resize, scroll | Page lifecycle |
| Drag & Drop | dragstart, drag, dragend, drop | Drag and drop |
| Touch | touchstart, touchmove, touchend | Mobile touch events |
π Adding Event Listeners
There are three ways to add event listeners in JavaScript. The modern, recommended approach is addEventListener().
Method 1: addEventListener() (Recommended)
// Syntax
element.addEventListener(eventType, handlerFunction, options);
// Example: Click event
const button = document.querySelector('#myButton');
button.addEventListener('click', function() {
console.log('Button clicked!');
});
// Arrow function syntax
button.addEventListener('click', () => {
console.log('Button clicked!');
});
// Named function (allows removal later)
function handleClick() {
console.log('Button clicked!');
}
button.addEventListener('click', handleClick);
// Multiple listeners on same element
button.addEventListener('click', () => {
console.log('First listener');
});
button.addEventListener('click', () => {
console.log('Second listener');
});
// Both will execute!
Method 2: Inline Event Handlers (Not Recommended)
<!-- β Not recommended - mixes HTML and JavaScript -->
<button onclick="alert('Clicked!')">Click Me</button>
<!-- β Not recommended - hard to maintain -->
<button onclick="handleClick()">Click Me</button>
<script>
function handleClick() {
alert('Clicked!');
}
</script>
Method 3: DOM Property (Limited)
// β οΈ Limited - can only have ONE handler per event type
const button = document.querySelector('#myButton');
button.onclick = function() {
console.log('First handler');
};
// This REPLACES the previous handler
button.onclick = function() {
console.log('Second handler'); // Only this one will run
};
Always use addEventListener(). It allows multiple listeners, is more flexible, and provides better control over event handling.
π¦ The Event Object
When an event occurs, the browser creates an event object containing information about the event and passes it to the handler function.
button.addEventListener('click', function(event) {
// event (or 'e') is the event object
console.log(event.type); // "click"
console.log(event.target); // Element that triggered the event
console.log(event.currentTarget); // Element listener is attached to
console.log(event.timeStamp); // When event occurred
});
// Commonly named 'e' or 'evt'
button.addEventListener('click', (e) => {
console.log(e.target);
});
// Mouse event properties
element.addEventListener('click', (e) => {
console.log(e.clientX); // Mouse X coordinate (viewport)
console.log(e.clientY); // Mouse Y coordinate (viewport)
console.log(e.pageX); // Mouse X coordinate (document)
console.log(e.pageY); // Mouse Y coordinate (document)
console.log(e.button); // Which mouse button (0=left, 1=middle, 2=right)
});
// Keyboard event properties
input.addEventListener('keydown', (e) => {
console.log(e.key); // Key that was pressed ("a", "Enter", "Escape")
console.log(e.code); // Physical key code ("KeyA", "Enter")
console.log(e.shiftKey); // true if Shift was held
console.log(e.ctrlKey); // true if Ctrl was held
console.log(e.altKey); // true if Alt was held
console.log(e.metaKey); // true if Meta/Cmd was held
});
π±οΈ Mouse Events
const box = document.querySelector('#box');
// Click events
box.addEventListener('click', () => {
console.log('Single click');
});
box.addEventListener('dblclick', () => {
console.log('Double click');
});
// Mouse down and up
box.addEventListener('mousedown', () => {
console.log('Mouse button pressed');
});
box.addEventListener('mouseup', () => {
console.log('Mouse button released');
});
// Mouse enter and leave
box.addEventListener('mouseenter', () => {
console.log('Mouse entered element');
box.style.backgroundColor = 'lightblue';
});
box.addEventListener('mouseleave', () => {
console.log('Mouse left element');
box.style.backgroundColor = '';
});
// Mouse move (fires continuously while moving)
box.addEventListener('mousemove', (e) => {
console.log(`Mouse at: ${e.clientX}, ${e.clientY}`);
});
// Context menu (right-click)
box.addEventListener('contextmenu', (e) => {
e.preventDefault(); // Prevent default context menu
console.log('Right-clicked!');
});
const tracker = document.querySelector('#tracker');
const display = document.querySelector('#display');
tracker.addEventListener('mousemove', (e) => {
const x = e.offsetX; // Mouse X relative to element
const y = e.offsetY; // Mouse Y relative to element
display.textContent = `Position: (${x}, ${y})`;
});
β¨οΈ Keyboard Events
const input = document.querySelector('#textInput');
// keydown - fires when key is pressed
input.addEventListener('keydown', (e) => {
console.log('Key pressed:', e.key);
// Check for specific keys
if (e.key === 'Enter') {
console.log('Enter key pressed!');
}
if (e.key === 'Escape') {
console.log('Escape pressed!');
}
});
// keyup - fires when key is released
input.addEventListener('keyup', (e) => {
console.log('Key released:', e.key);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + S (Save)
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault(); // Prevent browser save dialog
console.log('Save shortcut triggered!');
}
// Ctrl/Cmd + K
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
console.log('Search shortcut!');
}
});
// Arrow keys
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowUp':
console.log('Up arrow');
break;
case 'ArrowDown':
console.log('Down arrow');
break;
case 'ArrowLeft':
console.log('Left arrow');
break;
case 'ArrowRight':
console.log('Right arrow');
break;
}
});
The keypress event is deprecated. Use keydown or keyup instead.
π Form Events
const form = document.querySelector('#myForm');
const input = document.querySelector('#username');
const select = document.querySelector('#country');
// Submit event
form.addEventListener('submit', (e) => {
e.preventDefault(); // Prevent page reload
console.log('Form submitted!');
// Get form data
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log(data);
});
// Input event (fires on every change)
input.addEventListener('input', (e) => {
console.log('Current value:', e.target.value);
// Real-time validation
if (e.target.value.length < 3) {
console.log('Username too short');
}
});
// Change event (fires when input loses focus)
input.addEventListener('change', (e) => {
console.log('Input changed to:', e.target.value);
});
// Focus and blur events
input.addEventListener('focus', () => {
console.log('Input focused');
input.style.borderColor = 'blue';
});
input.addEventListener('blur', () => {
console.log('Input lost focus');
input.style.borderColor = '';
});
// Select/dropdown change
select.addEventListener('change', (e) => {
console.log('Selected:', e.target.value);
});
// Checkbox/radio events
const checkbox = document.querySelector('#agree');
checkbox.addEventListener('change', (e) => {
console.log('Checked:', e.target.checked);
});
π Document Events
// DOMContentLoaded - fires when HTML is parsed (before images load)
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM is ready!');
// Safe to manipulate DOM here
});
// load - fires when entire page is loaded (including images)
window.addEventListener('load', () => {
console.log('Page fully loaded!');
});
// beforeunload - fires before page unload (leaving page)
window.addEventListener('beforeunload', (e) => {
e.preventDefault();
e.returnValue = ''; // Show confirmation dialog
});
// Resize event
window.addEventListener('resize', () => {
console.log('Window resized:', window.innerWidth, window.innerHeight);
});
// Scroll event
window.addEventListener('scroll', () => {
console.log('Page scrolled:', window.scrollY);
});
// Scroll with throttling (better performance)
let isScrolling;
window.addEventListener('scroll', () => {
clearTimeout(isScrolling);
isScrolling = setTimeout(() => {
console.log('Scroll stopped');
}, 150);
});
π Event Propagation (Bubbling & Capturing)
When an event occurs on an element, it propagates through the DOM in two phases: capturing (down) and bubbling (up).
<div id="parent">
<button id="child">Click Me</button>
</div>
<script>
const parent = document.querySelector('#parent');
const child = document.querySelector('#child');
// Event bubbles up from child to parent
child.addEventListener('click', () => {
console.log('Child clicked');
});
parent.addEventListener('click', () => {
console.log('Parent clicked');
});
// When you click the button, both handlers fire:
// Output:
// Child clicked
// Parent clicked
</script>
Stopping Propagation
child.addEventListener('click', (e) => {
console.log('Child clicked');
e.stopPropagation(); // Stop event from bubbling up
});
parent.addEventListener('click', () => {
console.log('Parent clicked'); // This won't fire now
});
// stopImmediatePropagation() - stops other listeners on same element too
child.addEventListener('click', (e) => {
console.log('First listener');
e.stopImmediatePropagation();
});
child.addEventListener('click', () => {
console.log('Second listener'); // This won't fire
});
Capturing Phase
// Third parameter: useCapture (default false)
parent.addEventListener('click', () => {
console.log('Parent (capturing)');
}, true); // true = capturing phase
child.addEventListener('click', () => {
console.log('Child');
});
parent.addEventListener('click', () => {
console.log('Parent (bubbling)');
}, false); // false = bubbling phase (default)
// Click order:
// Parent (capturing) β Child β Parent (bubbling)
Complete event flow: Capturing Phase (window β target) β Target Phase β Bubbling Phase (target β window)
π― Event Delegation
Event delegation uses event bubbling to handle events on multiple children with a single listener on a parent element. This is more efficient and works with dynamically added elements.
// β Bad: Adding listener to each item
const items = document.querySelectorAll('.item');
items.forEach((item) => {
item.addEventListener('click', () => {
console.log('Item clicked');
});
});
// Problem: Doesn't work for items added later
// β
Good: Single listener on parent
const list = document.querySelector('#list');
list.addEventListener('click', (e) => {
// Check if clicked element matches selector
if (e.target.matches('.item')) {
console.log('Item clicked:', e.target.textContent);
}
});
// Works for dynamically added items too!
const newItem = document.createElement('li');
newItem.className = 'item';
newItem.textContent = 'New Item';
list.appendChild(newItem); // Click listener automatically works!
// More complex delegation with closest()
list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
console.log('Item clicked:', item.dataset.id);
}
const deleteBtn = e.target.closest('.delete-btn');
if (deleteBtn) {
const item = deleteBtn.closest('.item');
item.remove();
}
});
ποΈ Removing Event Listeners
// Must use named function to remove listener
function handleClick() {
console.log('Clicked!');
}
const button = document.querySelector('#myButton');
// Add listener
button.addEventListener('click', handleClick);
// Remove listener
button.removeEventListener('click', handleClick);
// β Won't work - different function reference
button.addEventListener('click', () => console.log('Click'));
button.removeEventListener('click', () => console.log('Click'));
// Pattern: One-time event listener
function handleOnce() {
console.log('This runs only once');
button.removeEventListener('click', handleOnce);
}
button.addEventListener('click', handleOnce);
// Modern alternative: once option
button.addEventListener('click', () => {
console.log('This runs only once');
}, { once: true });
π« Preventing Default Behavior
// Prevent form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
console.log('Form not submitted - handling with JS');
});
// Prevent link navigation
link.addEventListener('click', (e) => {
e.preventDefault();
console.log('Link not followed');
});
// Prevent context menu
element.addEventListener('contextmenu', (e) => {
e.preventDefault();
console.log('Custom context menu here');
});
// Prevent text selection
element.addEventListener('selectstart', (e) => {
e.preventDefault();
});
// Check if preventDefault was called
element.addEventListener('click', (e) => {
if (e.defaultPrevented) {
console.log('Default already prevented');
}
});
π¨ Custom Events
// Create custom event
const myEvent = new CustomEvent('userLoggedIn', {
detail: {
username: 'john_doe',
timestamp: Date.now()
},
bubbles: true,
cancelable: true
});
// Listen for custom event
document.addEventListener('userLoggedIn', (e) => {
console.log('User logged in:', e.detail.username);
console.log('At:', new Date(e.detail.timestamp));
});
// Dispatch custom event
document.dispatchEvent(myEvent);
// Practical example: Component communication
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
// Dispatch event when item added
const event = new CustomEvent('itemAdded', {
detail: { item, totalItems: this.items.length }
});
document.dispatchEvent(event);
}
}
// Listen for cart updates
document.addEventListener('itemAdded', (e) => {
console.log('Item added:', e.detail.item);
console.log('Total items:', e.detail.totalItems);
updateCartBadge(e.detail.totalItems);
});
π― Practical Examples
Example 1: Interactive Todo List
const todoForm = document.querySelector('#todoForm');
const todoInput = document.querySelector('#todoInput');
const todoList = document.querySelector('#todoList');
// Add todo
todoForm.addEventListener('submit', (e) => {
e.preventDefault();
const text = todoInput.value.trim();
if (!text) return;
// Create todo item
const li = document.createElement('li');
li.innerHTML = `
<span class="todo-text">${text}</span>
<button class="complete-btn">β</button>
<button class="delete-btn">β</button>
`;
todoList.appendChild(li);
todoInput.value = '';
});
// Event delegation for todo actions
todoList.addEventListener('click', (e) => {
// Complete todo
if (e.target.matches('.complete-btn')) {
const li = e.target.closest('li');
li.classList.toggle('completed');
}
// Delete todo
if (e.target.matches('.delete-btn')) {
const li = e.target.closest('li');
li.remove();
}
// Toggle by clicking text
if (e.target.matches('.todo-text')) {
const li = e.target.closest('li');
li.classList.toggle('completed');
}
});
Example 2: Keyboard Navigation
const items = document.querySelectorAll('.menu-item');
let currentIndex = 0;
function updateFocus() {
items.forEach((item, index) => {
item.classList.toggle('focused', index === currentIndex);
});
}
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
currentIndex = (currentIndex + 1) % items.length;
updateFocus();
}
if (e.key === 'ArrowUp') {
e.preventDefault();
currentIndex = (currentIndex - 1 + items.length) % items.length;
updateFocus();
}
if (e.key === 'Enter') {
items[currentIndex].click();
}
});
updateFocus();
Example 3: Drag and Drop
const draggables = document.querySelectorAll('.draggable');
const containers = document.querySelectorAll('.container');
// Make elements draggable
draggables.forEach((draggable) => {
draggable.addEventListener('dragstart', (e) => {
draggable.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', draggable.innerHTML);
});
draggable.addEventListener('dragend', () => {
draggable.classList.remove('dragging');
});
});
// Drop zones
containers.forEach((container) => {
container.addEventListener('dragover', (e) => {
e.preventDefault(); // Allow drop
e.dataTransfer.dropEffect = 'move';
container.classList.add('drag-over');
});
container.addEventListener('dragleave', () => {
container.classList.remove('drag-over');
});
container.addEventListener('drop', (e) => {
e.preventDefault();
container.classList.remove('drag-over');
const dragging = document.querySelector('.dragging');
container.appendChild(dragging);
});
});
Example 4: Debouncing and Throttling
// Debounce - wait until user stops typing
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Usage: Search input
const searchInput = document.querySelector('#search');
const handleSearch = debounce((e) => {
console.log('Searching for:', e.target.value);
// Make API call here
}, 500);
searchInput.addEventListener('input', handleSearch);
// Throttle - limit execution rate
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage: Scroll event
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);
β Best Practices
- Use
addEventListener()instead of inline handlers - Always use
preventDefault()when handling form submissions - Use event delegation for lists and dynamic content
- Remove event listeners when elements are removed (prevent memory leaks)
- Use named functions when you need to remove listeners later
- Debounce or throttle high-frequency events (scroll, resize, input)
- Use passive event listeners for scroll performance:
{ passive: true } - Check
event.targetvsevent.currentTargetwhen using delegation - Be careful with
stopPropagation()- it can break delegation patterns
- Events allow you to respond to user interactions and browser actions
- addEventListener() is the modern, flexible way to handle events
- Event object contains valuable information about the event
- Event propagation flows through capturing and bubbling phases
- Event delegation improves performance and works with dynamic content
- preventDefault() stops default browser behavior
- Custom events enable component communication
- Debouncing and throttling optimize performance for high-frequency events