JavaScript Events Explained
By Flavio Copes
Learn how JavaScript events work in the browser, from registering handlers with addEventListener to the Event object, bubbling, capturing, and throttling.
- Introduction
- Event handlers
- Listening on different elements
- The Event object
- Event bubbling and event capturing
- Stopping the propagation
- Event delegation
- Popular events
- Throttling
Introduction
JavaScript in the browser uses an event-driven programming model. You register a function, and the browser calls it when an event occurs.
An event can report a user action, a document lifecycle change, a network result, or a change produced by another API.
The official DOM events reference lists the main event families.
Event handlers
You respond to an event with an event handler, a function called when that event occurs.
With addEventListener(), you can register multiple listeners for the same event.
JavaScript offers three ways to register an event handler.
Inline event handlers
Inline handlers put JavaScript inside HTML:
<button onclick="doSomething()">Run</button>
Avoid this style in new code. It mixes markup and behavior, makes handlers harder to manage, and conflicts with strict Content Security Policy settings.
DOM on-event handlers
You can assign a function to an on... property:
window.onload = () => {
//window loaded
}
Assigning another function replaces the previous handler:
window.onload = () => {
console.log('second handler')
}
This is useful when you intentionally want at most one handler.
Using addEventListener()
addEventListener() lets you register multiple listeners without overwriting existing ones:
window.addEventListener('load', () => {
//window loaded
})
Use removeEventListener() with the same function reference when you need to remove a listener:
const handleClick = () => {
console.log('clicked')
}
const button = document.querySelector('button')
button.addEventListener('click', handleClick)
button.removeEventListener('click', handleClick)
Listener options
The third argument can be an options object:
button.addEventListener('click', handleClick, {
once: true
})
Useful options include:
capture, to listen during the capture phaseonce, to remove the listener after its first callpassive, to promise that the listener will not callpreventDefault()signal, to remove the listener when anAbortControlleris aborted
See the MDN addEventListener() reference for the complete behavior.
Listening on different elements
You can listen on window for events such as resize, on document for events such as DOMContentLoaded, or on a specific element for user interaction.
This is why addEventListener is sometimes called on window, sometimes on a DOM element.
The Event object
An event handler gets an Event object as the first parameter:
const link = document.getElementById('my-link')
link.addEventListener('click', event => {
// link clicked
})
Useful properties and methods include:
target, the object where the event originatedcurrentTarget, the object whose listener is currently runningtype, the type of eventbubbles, whether the event bubblescancelable, whether its default action can be canceledpreventDefault(), which prevents a cancelable default actionstopPropagation(), which stops the event moving farther through the DOM
Other properties are provided by specific kind of events, as Event is an interface for different specific events:
- MouseEvent
- KeyboardEvent
- DragEvent
- FetchEvent
- … and others
Each of those has a MDN page linked, so you can inspect all their properties.
For example when a KeyboardEvent happens, you can check which key was pressed, in a readable format (Escape, Enter and so on) by checking the key property:
window.addEventListener('keydown', event => {
// key pressed
console.log(event.key)
})
On pointer or mouse button events, button identifies the changed button:
const link = document.getElementById('my-link')
link.addEventListener('mousedown', event => {
// mouse button pressed
console.log(event.button) //0=primary, 1=middle, 2=secondary
})
Event bubbling and event capturing
Capturing, targeting, and bubbling are the phases an event can use while moving through the DOM.
Suppose your DOM structure is
<div id="container">
<button>Click me</button>
</div>
Suppose you have one listener on the button and another on #container.
Those event listeners will be called in order, and this order is determined by the event bubbling/capturing model used.
Bubbling means that the event propagates from the item that was clicked (the child) up to all its parent tree, starting from the nearest one.
In our example, the handler on button will fire before the #container handler.
Capturing is the opposite: the outer event handlers are fired before the more specific handler, the one on button.
Many common events bubble, including click. Some events, such as focus, do not. Check event.bubbles instead of assuming.
Listen during capture by setting the capture option:
document.getElementById('container').addEventListener('click', () => {
console.log('captured')
}, { capture: true })
During capture, the event travels toward the target. After reaching the target, a bubbling event travels back through its ancestors.
Stopping the propagation
A bubbling event moves through its ancestor elements unless you stop it.
<section>
<a id="my-link" href="/about">About</a>
</section>
A click event on a will propagate to section and then body.
Call stopPropagation() to stop the event from reaching more objects in its path:
const link = document.getElementById('my-link')
link.addEventListener('mousedown', event => {
// process the event
// ...
event.stopPropagation()
})
This does not prevent the browser’s default action. Use preventDefault() to stop a link navigation or form submission when the event is cancelable.
stopImmediatePropagation() also prevents later listeners on the same object from running.
Event delegation
Because many events bubble, you can listen on a parent instead of adding one listener to every child:
<ul id="menu">
<li><button data-action="save">Save</button></li>
<li><button data-action="delete">Delete</button></li>
</ul>
const menu = document.querySelector('#menu')
menu.addEventListener('click', event => {
const button = event.target.closest?.('button')
if (!button || !menu.contains(button)) return
console.log(button.dataset.action)
})
This is called event delegation. It also works for matching children added later.
Popular events
Here’s a list of the most common events you will likely handle.
Load
DOMContentLoaded fires when the HTML has been parsed and deferred or module scripts have run. It does not wait for images.
The load event on window fires after the page and its dependent resources finish loading. See the MDN DOMContentLoaded guide for the distinction.
Pointer and click events
click is device-independent. A mouse, touch interaction, keyboard, or assistive technology can activate it.
For coordinate-based input across mouse, pen, and touch, prefer pointer events such as pointerdown, pointermove, and pointerup. See the Pointer Events guide.
Keyboard events
keydown fires when a key is pressed, and can repeat while the key stays down. keyup fires when the key is released.
Scroll
The scroll event fires as a document or element scrolls. For the document position, check window.scrollY.
Keep in mind that this event is not a one-time thing. It fires a lot of times during scrolling, not just at the end or beginning of the scrolling, so don’t do any heavy computation or manipulation in the handler - use throttling instead.
Throttling
Events such as scroll and pointermove can fire many times. Heavy work in those handlers can make the page feel slow.
For visual updates, schedule at most one update per animation frame:
let scheduled = false
window.addEventListener('scroll', () => {
if (!scheduled) {
scheduled = true
requestAnimationFrame(() => {
console.log(window.scrollY)
scheduled = false
})
}
}, { passive: true })
This is not a general-purpose throttle, but it is a good pattern for scroll-linked rendering.
Related posts about js: