Skip to content

Event Options

Lit's @event=${handler} binding supports the same listener options native addEventListener does. This lesson covers the object-based syntax for configuring capture, once, and passive.

Passing Listener Options in a Template

By default, @event=${handler} attaches a listener with the browser's default options (bubble phase, not passive, not one-time). To pass additional addEventListener options — capture, once, or passive — provide an object with handleEvent and the option flags, instead of a plain function.

This object-based form is exactly what addEventListener itself accepts as a second argument (any object with a handleEvent method works as a listener), so Lit isn't introducing new syntax — it's exposing the platform's own capability declaratively.

html`
  <div
    @scroll=${{
      handleEvent: () => this.#onScroll(),
      passive: true,
    }}
  >
    ...
  </div>
`;

passive: true tells the browser this listener will never call preventDefault(), letting it optimize scroll performance.

The Listener Options Object

@event=${{
  handleEvent: (event) => { /* your logic */ },
  capture: true,   // listen during the capture phase
  once: true,      // automatically remove after firing once
  passive: true,   // promise not to call preventDefault()
}}
  • handleEvent replaces the plain function you'd normally pass — it receives the event exactly the same way.
  • capture: true listens during the event's capture phase (top-down) instead of the default bubble phase (bottom-up).
  • once: true automatically removes the listener after it fires a single time.
  • passive: true is a performance hint for scroll/touch listeners that never call preventDefault().

Event Options Cheat Sheet

When each addEventListener option is actually useful.

Option Use Case
capture: true Intercepting an event before a child's own handler runs
once: true A one-time confirmation click, or a tutorial dismiss button
passive: true High-frequency scroll/touchmove listeners for smoother scrolling
(none — default) The vast majority of ordinary click/input/change handlers

Changing Options Requires a New Listener Object

Lit detects whether to re-attach a listener by comparing the value passed to @event between renders. If you pass the same plain function reference every render, Lit reuses the existing listener efficiently. With the options-object form, make sure the object (or at least its handleEvent reference and option flags) is stable across renders if you don't want Lit to remove and re-add the listener unnecessarily.

// Stable across renders — defined once as a class field
#scrollListener = { handleEvent: () => this.#onScroll(), passive: true };

render() {
  return html`<div @scroll=${this.#scrollListener}>...</div>`;
}

Using the Capture Phase

The capture phase runs before an event reaches its target and before any bubble-phase listeners on descendants — useful when a container needs to intercept or log an interaction before a specific child element's own handler runs.

html`
  <div
    @click=${{ handleEvent: () => console.log('container saw it first'), capture: true }}
  >
    <button @click=${() => console.log('button handled it')}>Click</button>
  </div>
`;
// Logs "container saw it first" BEFORE "button handled it"

Common Mistakes

  • Creating a brand-new options object on every render, causing Lit to remove and re-add the listener unnecessarily on each update.
  • Setting passive: true on a listener that still calls preventDefault(), which browsers will ignore (and may warn about in the console).
  • Overusing capture: true when the default bubble phase would work fine and is easier to reason about.
  • Forgetting once: true removes the listener after a single firing — a second interaction with the same element won't trigger it again.

Key Takeaways

  • @event=${{ handleEvent, ...options }} exposes native addEventListener options declaratively in Lit templates.
  • capture, once, and passive behave exactly as they do with a manually attached listener.
  • Keep the options object reference stable across renders to avoid unnecessary listener churn.
  • passive: true is specifically valuable for high-frequency scroll/touch listeners.

Pro Tip

Reach for these advanced options only when you have a concrete reason (a measured scroll-jank issue, a genuine capture-phase requirement) — the plain @event=${handler} function form is simpler and correct for the overwhelming majority of event bindings.