Controlling the order of DOM event listeners

Posted . Visible to the public.

Event listeners are called in the order of their registration:

button.addEventListener('click', () => console.log("I run first"))
button.addEventListener('click', () => console.log("I run second"))

Sometimes you want a listener to always run first (or last), but have no control over the order in which other listeners are registered. There is no clean mechanism in the DOM API for this. This card shows some hacks to do it anyway.

Exploiting the capturing phase

Before an event bubbles up from an element to the document, it travels down from the document to the element. This is called the capturing phase.

You can have a listener call in the capture phase by passing an { capture: true } option. Since other code rarely uses capturing, this effectively causes your listener to run before existing listeners:

button.addEventListener('click', () => console.log("I run second"))
button.addEventListener('click', () => console.log("I run first"), { capture: true })

Binding to an ancestor

Listeners bound to an ancestor will still observe a (bubbling) event from its descendants. Since the event bubbles up from its target to the document, you can use a delegating listener to run after most existing listeners:

document.addEventListener('click', ({ target }) => {
  if (button.contains(target)) console.log("I run second")
})
button.addEventListener('click', () => console.log("I run first"))

Be mindful of memory leaks when registering listeners that way. In the example above, the listener registered to document will not be garbage collected when button is removed. You need to explicitly remove the listener when the button is detached.

In many cases you can find a different event that fires directly before or after the event you're interested in:

input.addEventListener('focus', () => console.log("I run second"))
input.addEventListener('focusin', () => console.log("I run first"))

Avoid event pairs that only go together some of the time. E.g. mousedown fires before click, but mousedown does not fire for keyboard users.

Profile picture of Henning Koch
Henning Koch
Last edit
Henning Koch
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2025-12-12 23:47)