LitElement
Importing LitElement
Section titled “Importing LitElement”Lit ships as a single npm package. In a build-tool project you install it once:
npm install litThen import the pieces you need:
import { LitElement, html, css } from 'lit';In the playground runner on this site we use a CDN dynamic import (the async IIFE pattern you have seen in earlier lessons):
const { LitElement, html, css } = await import('https://esm.sh/lit');What LitElement gives you
Section titled “What LitElement gives you”LitElement is a base class that extends the standard HTMLElement, adding three main things:
- Reactive properties — declared in
static properties; Lit watches them and schedules a re-render on change. - Declarative render — the
render()method returns aTemplateResultfrom thehtmltagged template literal. - Scoped styles — declared in
static styleswith thecsstagged template literal, adopted into the Shadow DOM by Lit.
static properties
Section titled “static properties”Declare reactive properties with the static class field static properties. Each key is a property name; the value is an options object with at minimum a type hint:
static properties = { count: { type: Number }, label: { type: String }, active: { type: Boolean },};When any of these properties changes, Lit automatically schedules an asynchronous re-render.
The decorator alternative
Section titled “The decorator alternative”If your project has a build step (TypeScript or Babel), Lit also offers a decorator-based API that is equivalent to static properties and registers the tag in one step with @customElement:
import { LitElement, html } from 'lit';import { customElement, property } from 'lit/decorators.js';
@customElement('my-counter')export class MyCounter extends LitElement { @property({ type: Number }) count = 0;
render() { return html`<p>Count: ${this.count}</p>`; }}TypeScript needs "experimentalDecorators": true in tsconfig.json (currently the recommended setting for production); Babel needs @babel/plugin-proposal-decorators. Because the playground on this page runs without a build step, its examples use the static properties field syntax instead — both compile down to the exact same reactive-property behavior.
render()
Section titled “render()”The render() method returns a TemplateResult — a tagged template literal processed by html`...`. Lit diffs the result against the previous render and patches only what changed:
render() { return html` <p>${this.label}: ${this.count}</p> <button @click=${() => this.count++}>Increment</button> `;}static styles
Section titled “static styles”Add scoped CSS with the static styles class field using the css`...` tagged template. Styles are adopted once into the element’s Shadow DOM stylesheet — they never leak out to the page:
static styles = css` :host { display: block; padding: 1rem; font-family: sans-serif; } button { background: #6200ee; color: white; border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer; } p { margin: 0 0 8px; }`;Reactive updates on property change
Section titled “Reactive updates on property change”When you write this.count++, the setter generated by Lit calls requestUpdate() internally. The update is batched at microtask timing, so multiple property changes in one tick produce a single render pass.
Interactive demo
Section titled “Interactive demo”Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Lit’s reactive properties (static properties) | Lit handles dirty-checking, batching, and re-rendering automatically, cutting a lot of boilerplate compared to writing it yourself | Adds roughly +5-6KB of runtime, plus some behavior (like microtask scheduling) you have to learn |
Manage state and re-render yourself in HTMLElement | Full control over every bit of timing, no added runtime | You write attribute parsing, dirty-checking, and DOM diffing entirely by hand |
Common mistakes
Section titled “Common mistakes”- Forgetting to declare a property in
static properties— without declaring it, Lit doesn’t track the value, so setting it doesn’t trigger a re-render of the UI. - Mutating an array/object property directly, e.g.
this.items.push(x), instead of assigning a new reference — Lit compares with!==by default, so it can’t detect the change and won’t re-render. - Putting heavy logic directly inside
render()instead of computing/caching it ahead of time —render()can be called more often than you’d expect, so this needlessly hurts performance.
💡 Real-world examples
Home Assistant — this home automation platform’s entire frontend is built from custom elements that extend
LitElementdirectly. (home-assistant/frontend)Google Material Web — Material Design’s components use
LitElement’s reactive properties and scoped styles as the core of every component’s implementation. (material-components/material-web)