Skip to content

Lit Components

This lesson explains how to build Lit components with LitElement, including component structure, lifecycle methods, events, slots, and the patterns used in production Lit Web Components.

What Is a Lit Component?

A Lit component is a custom element built by extending LitElement. It combines native Web Components with Lit's reactive rendering system, declarative templates, and scoped styles.

Every Lit component typically defines properties, styles, a render() method, and optional lifecycle callbacks for setup and cleanup.

Part Description
LitElement Base class for Lit components
@customElement() Registers the custom element tag
static styles Scoped component CSS
properties Reactive inputs and state
render() Returns the component template
Lifecycle methods Setup, updates, and cleanup hooks

Basic Lit Component Example

import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";

@customElement("app-alert")
class AppAlert extends LitElement {
  @property()
  type = "info";

  @property()
  message = "";

  static styles = css`
    .alert {
      padding: 1rem;
      border-radius: 0.5rem;
    }
    .info { background: #dbeafe; color: #1e3a8a; }
    .success { background: #dcfce7; color: #166534; }
  `;

  render() {
    return html`
      <div class="alert ${this.type}" role="alert">
        ${this.message}
      </div>
    `;
  }
}
<app-alert
  type="success"
  message="Profile updated successfully."
></app-alert>

This component combines registration, reactive properties, scoped styles, and a declarative template in one class.

Lit Component Anatomy

@customElement("user-card")
class UserCard extends LitElement {
  @property() name = "";
  @property() role = "";

  static styles = css`...`;

  connectedCallback() {
    super.connectedCallback();
  }

  render() {
    return html`...`;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
  }
}
Section Purpose
Decorator / registration Defines the custom element tag
Properties Public reactive API
static styles Component CSS in Shadow DOM
render() Component markup and bindings
Lifecycle methods Setup, update, and cleanup logic

Lit Lifecycle Methods

connectedCallback() {
  super.connectedCallback();
  console.log("Component added to DOM");
}

disconnectedCallback() {
  super.disconnectedCallback();
  console.log("Component removed from DOM");
}

updated(changedProperties) {
  if (changedProperties.has("open")) {
    this.handleOpenChange();
  }
}

firstUpdated() {
  this.focusTrigger();
}
Method When It Runs
connectedCallback() Component added to the DOM
disconnectedCallback() Component removed from the DOM
willUpdate() Before a reactive update
updated() After a reactive update
firstUpdated() After the first render completes

Emitting Events from Lit Components

handleSave() {
  this.dispatchEvent(
    new CustomEvent("save-click", {
      detail: { id: this.itemId },
      bubbles: true,
      composed: true
    })
  );
}

render() {
  return html`
    <button type="button" @click="${this.handleSave}">
      Save
    </button>
  `;
}

Lit components communicate with parent apps through custom events, just like native Web Components.

Slots in Lit Components

render() {
  return html`
    <article class="card">
      <header>
        <slot name="title"></slot>
      </header>
      <div class="body">
        <slot></slot>
      </div>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </article>
  `;
}
<lit-card>
  <h2 slot="title">Team Updates</h2>
  <p>Design review is scheduled for Monday.</p>
  <button slot="footer" type="button">View Calendar</button>
</lit-card>

Lit components support default and named slots for composable layouts.

Shadow DOM in Lit Components

static styles = css`
  :host {
    display: block;
  }

  :host([disabled]) {
    opacity: 0.6;
    pointer-events: none;
  }

  button {
    background: var(--btn-bg, #2563eb);
    color: white;
  }
`;

Lit renders into Shadow DOM by default, giving components style encapsulation and a clean internal structure.

Lit Components Without Decorators

class CounterBadge extends LitElement {
  static properties = {
    count: { type: Number }
  };

  static styles = css`
    span { font-weight: 600; }
  `;

  constructor() {
    super();
    this.count = 0;
  }

  render() {
    return html`<span>Count: ${this.count}</span>`;
  }
}

customElements.define("counter-badge", CounterBadge);

You can build Lit components without decorators by using static properties and customElements.define().

Organizing Lit Components

src/
├── components/
│   ├── app-alert.js
│   ├── app-button.js
│   └── user-card.js
├── styles/
│   └── tokens.css
└── index.js
  • Keep one component per file when possible.
  • Export components from a central entry point.
  • Share design tokens through CSS custom properties.
  • Document public properties, slots, and events.
  • Keep rendering logic separate from data-fetching logic.

Lit Components vs Vanilla Custom Elements

Feature Lit Component Vanilla Custom Element
Rendering render() with html`...` Manual DOM updates
Reactivity Built in Manual
Styles static styles Shadow CSS manually
Boilerplate Lower Higher

When to Build Lit Components

  • You want reusable Web Components with less boilerplate.
  • You need reactive updates from properties and state.
  • You are creating a design system or shared UI library.
  • You want declarative templates and scoped styles.
  • You need components that work across frameworks.
  • You already understand native Web Components basics.

Common Lit Component Use Cases

  • Buttons, badges, alerts, and form controls.
  • Cards, dialogs, drawers, and panels.
  • Tabs, menus, and navigation widgets.
  • Data display components with reactive inputs.
  • Embeddable widgets for third-party applications.
  • Framework-agnostic company design systems.

Lit Component Best Practices

  • Keep components small and focused on one responsibility.
  • Use render() for view logic only.
  • Call super.connectedCallback() and super.disconnectedCallback().
  • Emit documented custom events for parent communication.
  • Use slots for flexible composition.
  • Expose theming through CSS custom properties.
  • Clean up listeners and timers in lifecycle hooks.

Common Lit Component Mistakes

  • Putting side effects inside render().
  • Forgetting to call super in lifecycle methods.
  • Mixing imperative DOM updates with Lit templates.
  • Not documenting the public component API.
  • Creating overly large components with too many responsibilities.
  • Skipping accessibility in custom interactive elements.
  • Not cleaning up resources on disconnect.

Key Takeaways

  • Lit components extend LitElement and use render().
  • Properties, styles, templates, and lifecycle methods form the core structure.
  • Lit components are still native Web Components under the hood.
  • Events, slots, and Shadow DOM work the same as in vanilla components.
  • Good Lit component design leads to reusable, maintainable UI libraries.

Pro Tip

Structure every Lit component the same way: register the element, declare properties, define styles, implement render(), and use lifecycle methods only for setup, reactions, and cleanup.