LitElement Basics — Building Reactive Components with LitElement
In this tutorial, you will learn about LitElement Basics. We cover key concepts, practical examples, and best practices to help you master this topic.
LitElement is the modern base class for building fast, lightweight Web Components. It combines Lit's reactive property system with lit-html's efficient template rendering.
What You'll Learn
- LitElement component structure
- Static styles template
- Reactive properties with decorators
- Render method with lit-html
- LitElement lifecycle
Why It Matters
LitElement is the evolution of Polymer — smaller, faster, and standards-based. It powers production component libraries like the Adobe Spectrum design system.
Real-World Use
A design system with 100+ components built on LitElement, sharing styles and utilities, rendered efficiently with lit-html.
LitElement Architecture
flowchart TD
A[LitElement] --> B[Base Class]
A --> C[Properties]
A --> D[Styles]
A --> E[Render]
A --> F[Lifecycle]
B --> G[HTMLElement]
C --> H[Decorators]
C --> I[Options]
D --> J[Static styles]
E --> K[lit-html]
F --> L[Update Cycle]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Basic LitElement Component
import { LitElement, html, css } from 'lit';
class GreetingCard extends LitElement {
static styles = css`
:host {
display: block;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
}
h2 { margin: 0 0 8px; color: #1a237e; }
p { margin: 0; color: #666; }
`;
static properties = {
name: { type: String },
greeting: { type: String }
};
constructor() {
super();
this.name = 'World';
this.greeting = 'Hello';
}
render() {
return html`
<h2>${this.greeting}, ${this.name}!</h2>
<p>Welcome to LitElement</p>
`;
}
}
customElements.define('greeting-card', GreetingCard);
<greeting-card name="Alice" greeting="Hi"></greeting-card>
Expected output: A card showing "Hi, Alice!" with LitElement's reactive binding. Property changes re-render efficiently.
Reactive Properties with Options
import { LitElement, html, css } from 'lit';
class ProgressIndicator extends LitElement {
static styles = css`
:host { display: block; }
.track { height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden; }
.fill { height: 100%; background: var(--progress-color, #1a237e); transition: width 0.3s; border-radius: 4px; }
.label { font-size: 12px; margin-top: 4px; text-align: right; color: #666; }
`;
static properties = {
value: { type: Number, reflect: true },
max: { type: Number },
color: { type: String, attribute: 'data-color' },
_internal: { state: true }
};
constructor() {
super();
this.value = 0;
this.max = 100;
this.color = '#1a237e';
this._internal = 0;
}
get _percent() {
return Math.min(100, Math.max(0, (this.value / this.max) * 100));
}
render() {
return html`
<div class="track">
<div class="fill" style="width:${this._percent}%;background:${this.color}"></div>
</div>
<div class="label">${Math.round(this._percent)}%</div>
`;
}
}
customElements.define('progress-indicator', ProgressIndicator);
Expected output: reflect: true syncs value to attribute. state: true properties are internal-only. type controls Parsing.
Static Styles
import { LitElement, html, css } from 'lit';
const sharedStyles = css`
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
.btn-primary { background: #1a237e; color: white; }
.btn-secondary { background: #e0e0e0; color: #333; }
.btn-danger { background: #c62828; color: white; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
`;
class ButtonSet extends LitElement {
static styles = [sharedStyles, css`
:host { display: flex; gap: 8px; align-items: center; }
`];
render() {
return html`
<button class="btn btn-primary"><slot name="primary"></slot></button>
<button class="btn btn-secondary"><slot name="secondary"></slot></button>
`;
}
}
customElements.define('button-set', ButtonSet);
class DangerButton extends LitElement {
static styles = [sharedStyles, css`
:host { display: inline-block; }
`];
render() {
return html`<button class="btn btn-danger"><slot></slot></button>`;
}
}
customElements.define('danger-button', DangerButton);
Expected output: Shared styles are imported and combined. Array of styles merges into component. No duplication in output.
Conditional Rendering
import { LitElement, html, css } from 'lit';
class TogglePanel extends LitElement {
static styles = css`
:host { display: block; }
.panel { padding: 16px; border: 1px solid #e0e0e0; border-radius: 8px; margin-top: 8px; }
.hidden { display: none; }
`;
static properties = {
open: { type: Boolean, reflect: true },
title: { type: String }
};
constructor() { super(); this.open = false; this.title = 'Panel'; }
render() {
return html`
<button @click=${() => this.open = !this.open}>
${this.open ? 'Hide' : 'Show'} ${this.title}
</button>
${this.open ? html`
<div class="panel">
<slot></slot>
</div>
` : ''}
`;
}
}
customElements.define('toggle-panel', TogglePanel);
Expected output: Conditional rendering with ternary inside template literal. Only renders panel content when open is true.
Lists and Iteration
import { LitElement, html, css } from 'lit';
class TaskList extends LitElement {
static styles = css`
.task { padding: 8px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px; }
.done { text-decoration: line-through; color: #999; }
.empty { padding: 16px; text-align: center; color: #999; }
`;
static properties = {
tasks: { type: Array },
filter: { type: String }
};
constructor() {
super();
this.tasks = [
{ id: 1, text: 'Learn LitElement', done: true },
{ id: 2, text: 'Build components', done: false },
{ id: 3, text: 'Write tests', done: false }
];
this.filter = 'all';
}
get _filtered() {
if (this.filter === 'active') return this.tasks.filter(t => !t.done);
if (this.filter === 'done') return this.tasks.filter(t => t.done);
return this.tasks;
}
toggle(id) {
this.tasks = this.tasks.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
render() {
return html`
<div>
${['all', 'active', 'done'].map(f => html`
<button @click=${() => this.filter = f}
class="${this.filter === f ? 'active' : ''}">${f}</button>
`)}
</div>
${this._filtered.length === 0 ? html`<div class="empty">No tasks</div>` : html`
${this._filtered.map(task => html`
<div class="task">
<input type="checkbox" ?checked=${task.done} @change=${() => this.toggle(task.id)}>
<span class="${task.done ? 'done' : ''}">${task.text}</span>
</div>
`)}
`}
`;
}
}
customElements.define('task-list', TaskList);
Expected output: Array.map inside render() produces list items. Conditionals handle empty state and filter buttons.
Lifecycle Methods
import { LitElement, html } from 'lit';
class LifecycleDemo extends LitElement {
static properties = { data: { type: Object } };
constructor() { super(); this.data = null; }
connectedCallback() {
super.connectedCallback();
console.log('Connected');
this._fetchData();
}
disconnectedCallback() {
super.disconnectedCallback();
console.log('Disconnected');
this._cleanup();
}
willUpdate(changedProperties) {
if (changedProperties.has('data')) {
console.log('Data will update');
}
}
updated(changedProperties) {
if (changedProperties.has('data')) {
this.dispatchEvent(new CustomEvent('data-loaded', {
detail: this.data
}));
}
}
firstUpdated(changedProperties) {
console.log('First render complete');
}
shouldUpdate(changedProperties) {
if (this.data && !changedProperties.has('data')) return false;
return true;
}
async _fetchData() {
const response = await fetch('/api/data');
this.data = await response.json();
}
_cleanup() { /* teardown */ }
render() {
return html`<pre>${JSON.stringify(this.data, null, 2)}</pre>`;
}
}
customElements.define('lifecycle-demo', LifecycleDemo);
Expected output: Lifecycle hooks run in order: constructor -> connectedCallback -> willUpdate -> render -> updated -> firstUpdated. shouldUpdate controls re-rendering.
Common Mistakes
Forgetting to call super in lifecycle - Always call super.connectedCallback(), super.disconnectedCallback().
Mutating arrays/objects in place - Use immutable updates for reactive properties.
Heavy compute in render() - Use getters or willUpdate for expensive work.
Not cleaning up external listeners - Remove window/document listeners in disconnectedCallback.
Calling requestUpdate unnecessarily - LitElement detects property changes automatically.
Practice Questions
- How does LitElement differ from Polymer 3?
- What is the purpose of static styles in LitElement?
- How do you conditionally render content in lit-html?
- What lifecycle hook runs after first render completes?
- How do you prevent a re-render in LitElement?
Challenge: Build a movie browser with search, filter by genre, sort by rating/year, card list/grid toggle, loading states, pagination, and movie detail dialog — all with LitElement.
FAQ
Mini Project
Build a product gallery with: product card component, category filter, price range slider, sort controls, cart with add/remove, and order summary — all LitElement components composing together.
What's Next
LitElement basics provide the foundation. Learn LitElement Advanced for directives, slots, and performance optimization.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro