Add an event listener with the options you actually need
Canonical URL: https://devexamples.com/javascript/add-event-listener-with-options/
The problem
You attach a DOM listener and the two-argument call leaves every policy at its default: the listener lives for the page lifetime, it runs in the bubble phase, and a wheel or scroll handler can stall the compositor. You need one-shot registration, capture-phase visibility, and a guaranteed-non-blocking gesture listener on elements that exist when the script runs.
Short answer
Pass an options object as the third argument — once for automatic removal after the first event, capture to run in the capture phase before descendant listeners, passive to promise that preventDefault is never called so gestures stay smooth.
Calling addEventListener with only a type and a handler accepts the browser’s defaults for every policy that matters: the listener survives until you remove it by hand, it runs in the bubble phase, and on a wheel or scroll event the browser delays the gesture until it knows whether your handler will cancel it. The third argument is where each of those decisions belongs.
<aside id="trial-notice">
<p>Your trial ends in three days.</p>
<button type="button">Dismiss</button>
</aside>const notice = document.querySelector('#trial-notice');
const dismiss = notice.querySelector('button');
// once — the browser removes this listener after its first invocation.
dismiss.addEventListener(
'click',
() => {
notice.hidden = true;
console.log('notice dismissed');
},
{ once: true }
);
// capture — runs on the way down, before any listener on the button itself.
notice.addEventListener(
'click',
(event) => {
console.log('captured click on', event.target.textContent);
},
{ capture: true }
);
// passive — a promise that preventDefault is never called, so the compositor scrolls freely.
window.addEventListener(
'wheel',
(event) => {
console.log('wheel delta', event.deltaY);
},
{ passive: true }
);Explanation
The options object is read once, at registration, and stored on the listener record, which is why each flag changes a different part of the browser’s machinery. once makes the dispatch steps remove the listener before invoking it, so the handler retires itself even if it throws — the line you no longer write is the removeEventListener call whose function reference never quite matched. capture registers the listener for the capture phase, the walk from the window down toward the target, so it observes the event before the target’s own handlers can stop propagation. passive is a promise about what the handler will never do: because the browser may assume preventDefault will not be called, it can scroll or zoom on the compositor without waiting for the handler to return.
One flag per listener is deliberate. Every option defaults to false, and passing an options object only to repeat the defaults hides the one decision the registration actually makes, so the example attaches each flag to the handler whose behaviour depends on it and leaves the rest at the plain call.
The boundary condition sits in listener identity: the same function registered on the same element with capture: true and with the default capture: false produces two separate listeners, and only the capture value — not once or passive — participates in the match that removeEventListener performs. A second boundary is the passive promise itself: a preventDefault call inside a passive wheel listener has no effect and earns a console warning, so the flag is correct for read-only handlers such as analytics or the logging call above, and wrong for a custom zoom control that must cancel ctrl plus wheel.
Next steps
A correctly registered listener still assumes one element per binding. When the page adds or removes elements after load, move to a single handler on a stable container; when the events arrive faster than the handler can usefully run, collapse the burst with a timer before the work, which is a debounce rather than a registration change.
Usage notes
- The options object is read once when you register; changing a property afterwards does not alter the live listener.
- removeEventListener matches a listener by type, function reference, and capture value, so a capture-phase listener must be removed with the same capture flag.
Caveats
- Inside a passive listener a call to preventDefault is ignored and the browser logs a warning; mark a listener passive only when it genuinely never cancels the event.
Follow-up
Follow-up
Delegate events for a list that changes after loadOne listener on the container rather than one per row is the next step once the page adds elements after load.
Examples that treat this as a prerequisite
Derived at build time from the editorial graph; no edge is invented here.
- Validate form input before the browser submits it
This handler is an event listener, so the options that decide when it runs and how it is removed come first.
Related examples
Editorial links first, then deterministic same-task or same-topic candidates. Tags and shared language alone never qualify a candidate.
Related
Debounce a scroll handler without dropping the last callBoth examples decide when a handler runs: this one fixes the policy at registration, debounce fixes it per event.
Same task or topic
Delegate events for a list that changes after loadSolves the Handle DOM events task
Same task or topic
Announce field errors without moving the caretShares the DOM Events topic
References
- EventTarget: addEventListener() method(opens in a new tab) — MDN Web Docs. Reference for the options object members once, capture, and passive.
- DOM Standard — EventTarget interfaces(opens in a new tab) — WHATWG. Normative listener registration and removal steps.
Source page: https://devexamples.com/javascript/add-event-listener-with-options/