Events in JavaScript
Events are actions that happen in the browser — clicks, key presses, form submissions, page load, and more. JavaScript responds to events with event listeners (handlers).
Adding Event Listeners
const button = document.querySelector('#myBtn');
button.addEventListener('click', function() {
console.log('Button clicked!');
});
button.addEventListener('click', () => {
console.log('Clicked with arrow function');
});
Avoid inline handlers like onclick="..." in HTML — they mix concerns, block CSP, and are harder to maintain.
The Event Object
Handlers receive an event object with useful properties and methods:
button.addEventListener('click', (event) => {
console.log(event.type); // 'click'
console.log(event.target); // element that triggered the event
console.log(event.currentTarget); // element listener is attached to
event.preventDefault(); // cancel default (form submit, link navigation)
event.stopPropagation(); // stop bubbling to parents
event.stopImmediatePropagation(); // stop other listeners on same element
});
Common Event Types
| Event | Fires when |
|---|---|
click |
Element clicked |
dblclick |
Double-click |
mousedown / mouseup |
Mouse button pressed/released |
mouseenter / mouseleave |
Mouse enters/leaves (no bubble) |
mouseover / mouseout |
Mouse enters/leaves (bubbles) |
keydown / keyup / keypress |
Key events |
input |
Input value changes (every keystroke) |
change |
Value committed (select, checkbox, blur after edit) |
submit |
Form submitted |
focus / blur |
Element gains/loses focus |
load |
Resource finished loading |
DOMContentLoaded |
HTML parsed, DOM ready |
scroll |
Element scrolled |
resize |
Window resized |
Keyboard Events
document.addEventListener('keydown', (e) => {
console.log(e.key); // 'Enter', 'a', 'Shift'
console.log(e.code); // physical key: 'KeyA', 'Enter'
console.log(e.ctrlKey); // modifier keys
console.log(e.shiftKey);
console.log(e.altKey);
console.log(e.metaKey); // Cmd on Mac
if (e.key === 'Escape') closeModal();
if (e.key === 'Enter' && e.ctrlKey) submitForm();
});
Use e.key for character input; e.code for game controls or shortcuts independent of layout.
Form Events
<form id="myForm">
<input id="email" type="email" required>
<button type="submit">Submit</button>
</form>
<script>
document.querySelector('#myForm').addEventListener('submit', (e) => {
e.preventDefault();
const email = document.querySelector('#email').value;
console.log('Submitted:', email);
});
document.querySelector('#email').addEventListener('input', (e) => {
console.log('Typing:', e.target.value);
});
</script>
Event Propagation: Capturing and Bubbling
Events travel in three phases:
- Capturing — from
windowdown to target - Target — event reaches target element
- Bubbling — from target back up to
window
document.querySelector('#parent').addEventListener('click', () => {
console.log('Parent — bubble phase');
});
document.querySelector('#child').addEventListener('click', (e) => {
console.log('Child — bubble phase');
// e.stopPropagation(); // prevents parent handler
});
// Capturing phase (third argument true, or { capture: true })
document.querySelector('#parent').addEventListener('click', () => {
console.log('Parent — capture phase');
}, { capture: true });
Default: listeners run in bubbling phase.
Event Delegation
Attach one listener to a parent for dynamic children:
document.querySelector('#list').addEventListener('click', (e) => {
if (e.target.matches('li')) {
e.target.classList.toggle('done');
}
if (e.target.matches('.delete-btn')) {
e.target.closest('li').remove();
}
});
Benefits: works for future elements, fewer listeners, better memory use.
Listener Options
button.addEventListener('click', handler, {
once: true, // auto-remove after first invocation
passive: true, // won't call preventDefault — scroll performance
capture: false, // bubble phase (default)
signal: controller.signal // AbortController for cleanup
});
const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // removes all listeners with this signal
Use { passive: true } on touch and scroll listeners unless you need preventDefault().
Removing Listeners
function handleClick() {
console.log('Clicked');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
// Must pass same function reference — anonymous functions can't be removed
Custom Events
const event = new CustomEvent('userLoggedIn', {
detail: { username: 'alice' },
bubbles: true
});
document.dispatchEvent(event);
document.addEventListener('userLoggedIn', (e) => {
console.log(e.detail.username);
});
Complete Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Events Example</title>
</head>
<body>
<button id="btn">Click me</button>
<p id="output">Waiting...</p>
<script>
const btn = document.querySelector('#btn');
const output = document.querySelector('#output');
let count = 0;
btn.addEventListener('click', () => {
count++;
output.textContent = `Clicked ${count} time(s)`;
});
document.addEventListener('keydown', (e) => {
if (e.key === 'r' && !e.ctrlKey) {
count = 0;
output.textContent = 'Reset!';
}
});
</script>
</body>
</html>
Best Practices
- Use
addEventListener— not inline HTML handlers - Delegate for lists and dynamic content
- Debounce input handlers for search (see Advanced Functions)
- Passive scroll listeners for performance
- Clean up listeners when components unmount (SPA frameworks)
Troubleshooting
Handler fires twice
- Listener registered multiple times; check for duplicate
addEventListener - Event bubbling — use
stopPropagationif intentional single handler
preventDefault not working
- Listener may be passive — remove
passive: true - Event already completed — call during synchronous handler
Keyboard shortcut conflicts with browser
- Use
e.preventDefault()in handler; checke.targetisn’t an input
Events connect user actions to code — understand propagation, delegation, and the event object for robust interactive applications.