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.