Skip to content

Lit Events

Lit doesn't invent its own event system — it uses the browser's native DOM event model, with a convenient @event=${handler} binding syntax layered on top. This lesson gives an overview before diving into handling and dispatching in depth.

Events in Lit Are Native DOM Events

Every event your Lit component listens to or dispatches — clicks, input changes, custom application events — is a standard Event (or CustomEvent) object, dispatched through the same addEventListener/dispatchEvent machinery every other web page uses. Lit's @event=${handler} template syntax is simply a declarative shorthand for calling addEventListener for you.

This matters because it means everything you already know about DOM events — bubbling, capturing, preventDefault(), stopPropagation() — applies directly and without any Lit-specific caveats, aside from a few Shadow DOM-specific nuances around event composition covered in a later lesson.

class ClickCounter extends LitElement {
  @state() private clicks = 0;

  render() {
    return html`
      <button @click=${this.#onClick}>
        Clicked ${this.clicks} times
      </button>
    `;
  }

  #onClick(event) {
    this.clicks++;
    console.log(event.type, event.target); // a real, native click Event
  }
}

event inside the handler is a genuine, native Event object — nothing about it is Lit-specific.

Listening vs. Dispatching

// Listening (inside a template)
html`<button @click=${this.#onClick}>Click</button>`;

// Dispatching (from your component)
this.dispatchEvent(new CustomEvent('item-selected', { detail: { id: 1 } }));
  • @event=${handler} is the declarative way to listen for both native events (click, input) and custom events dispatched by children.
  • this.dispatchEvent(new CustomEvent(...)) is how a Lit component notifies its parent (or any ancestor) that something happened.
  • Custom event names are conventionally lowercase and hyphenated, similar to HTML attribute naming, e.g. item-selected.
  • Native events (click, input, change) never need bubbles: true set explicitly — most already bubble by default.

Lit Events at a Glance

The core vocabulary for working with events in Lit.

Concept Example Purpose
Listen declaratively @click=${this.#onClick} Attach a listener via the template
Listen imperatively this.addEventListener('click', fn) Attach a listener outside a template
Dispatch a custom event this.dispatchEvent(new CustomEvent('name')) Notify ancestors of something
Attach event data new CustomEvent('name', { detail: {...} }) Pass structured data with the event
Cross the shadow boundary { composed: true } Let the event escape this component's Shadow DOM
Stop it bubbling further event.stopPropagation() Prevent ancestors from seeing the event

Events Flow Both Ways: Down via Properties, Up via Events

A consistent, idiomatic pattern in Lit (and Web Components generally) is: data flows *down* into a component via properties, and notifications flow *up* out of a component via dispatched custom events. A child component should never reach up and mutate a parent's state directly — it should dispatch an event and let the parent decide how to respond.

  • Parent sets .items=${this.items} on a child — data flowing down as a property.
  • Child dispatches item-selected when the user picks something — a notification flowing up as an event.
  • The parent listens with @item-selected=${this.#onItemSelected} and updates its own state in response.

Native Events vs. Custom Events

Native events (click, input, change, keydown) are dispatched automatically by the browser in response to user interaction. Custom events are ones *your* component explicitly creates and dispatches to communicate something specific to your application's domain, like item-selected or form-submitted.

Event Type Who Dispatches It Example
Native The browser, automatically click, input, keydown
Custom Your component's own code item-selected, value-changed

Common Mistakes

  • Having a child component directly mutate a property on its parent instead of dispatching an event and letting the parent decide how to react.
  • Forgetting that custom events need bubbles: true explicitly if you want them to be heard by an ancestor beyond the immediate parent.
  • Confusing native browser events with custom application events when reasoning about where a listener should be attached.
  • Attaching the same listener repeatedly on every render instead of relying on Lit's own event binding, which reuses the listener automatically.

Key Takeaways

  • Lit events are ordinary native DOM Event/CustomEvent objects — no special Lit-specific event system exists.
  • @event=${handler} is declarative sugar over addEventListener.
  • The idiomatic Lit pattern is data down via properties, notifications up via dispatched events.
  • Native events are browser-dispatched; custom events are dispatched explicitly by your own component code.

Pro Tip

Whenever you're deciding between 'should this be a property or an event', ask which direction the information flows: configuration and data flow down as properties, notifications about something that happened flow up as events.