Skip to content

LitElement

Lit ships as a single npm package. In a build-tool project you install it once:

Terminal window
npm install lit

Then 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');

LitElement is a base class that extends the standard HTMLElement, adding three main things:

  1. Reactive properties — declared in static properties; Lit watches them and schedules a re-render on change.
  2. Declarative render — the render() method returns a TemplateResult from the html tagged template literal.
  3. Scoped styles — declared in static styles with the css tagged template literal, adopted into the Shadow DOM by Lit.

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.

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.

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>
`;
}

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; }
`;

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.

OptionBenefitCost
Lit’s reactive properties (static properties)Lit handles dirty-checking, batching, and re-rendering automatically, cutting a lot of boilerplate compared to writing it yourselfAdds roughly +5-6KB of runtime, plus some behavior (like microtask scheduling) you have to learn
Manage state and re-render yourself in HTMLElementFull control over every bit of timing, no added runtimeYou write attribute parsing, dirty-checking, and DOM diffing entirely by hand
  • 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 LitElement directly. (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)

What does declaring a property in static properties do?
What does the html tagged template literal return?
How does Lit scope CSS to only the component's shadow tree?