# JavaScript Events Explained

> Learn how JavaScript events work in the browser, from registering handlers with addEventListener to the Event object, bubbling, capturing, and throttling.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2018-04-10 | Updated: 2026-07-18 | Topics: [JavaScript](https://flaviocopes.com/tags/js/) | Canonical: https://flaviocopes.com/javascript-events/

<!-- TOC -->

- [Introduction](#introduction)
- [Event handlers](#event-handlers)
  - [Inline event handlers](#inline-event-handlers)
  - [DOM on-event handlers](#dom-on-event-handlers)
  - [Using `addEventListener()`](#using-addeventlistener)
  - [Listener options](#listener-options)
- [Listening on different elements](#listening-on-different-elements)
- [The Event object](#the-event-object)
- [Event bubbling and event capturing](#event-bubbling-and-event-capturing)
- [Stopping the propagation](#stopping-the-propagation)
- [Event delegation](#event-delegation)
- [Popular events](#popular-events)
  - [Load](#load)
  - [Pointer and click events](#pointer-and-click-events)
  - [Keyboard events](#keyboard-events)
  - [Scroll](#scroll)
- [Throttling](#throttling)

<!-- /TOC -->

## Introduction

[JavaScript](https://flaviocopes.com/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](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events) 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:

```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:

```js
window.onload = () => {
  //window loaded
}
```

Assigning another function replaces the previous handler:

```js
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:

```js
window.addEventListener('load', () => {
  //window loaded
})
```

Use `removeEventListener()` with the same function reference when you need to remove a listener:

```js
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:

```js
button.addEventListener('click', handleClick, {
  once: true
})
```

Useful options include:

- `capture`, to listen during the capture phase
- `once`, to remove the listener after its first call
- `passive`, to promise that the listener will not call `preventDefault()`
- `signal`, to remove the listener when an `AbortController` is aborted

See the [MDN `addEventListener()` reference](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) 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:

```js
const link = document.getElementById('my-link')
link.addEventListener('click', event => {
  // link clicked
})
```

Useful properties and methods include:

- `target`, the object where the event originated
- `currentTarget`, the object whose listener is currently running
- `type`, the type of event
- `bubbles`, whether the event bubbles
- `cancelable`, whether its default action can be canceled
- `preventDefault()`, which prevents a cancelable default action
- `stopPropagation()`, which stops the event moving farther through the DOM

([see the full list](https://developer.mozilla.org/en-US/docs/Web/API/Event)).

Other properties are provided by specific kind of events, as `Event` is an interface for different specific events:

- [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)
- [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)
- [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent)
- [FetchEvent](https://developer.mozilla.org/en-US/docs/Web/API/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:

```js
window.addEventListener('keydown', event => {
  // key pressed
  console.log(event.key)
})
```

On pointer or mouse button events, `button` identifies the changed button:

```js
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

```html
<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:

```js
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.

```html
<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:

```js
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:

```html
<ul id="menu">
  <li><button data-action="save">Save</button></li>
  <li><button data-action="delete">Delete</button></li>
</ul>
```

```js
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](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event) 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](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events).

### 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:

```js
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.
