Web Components with Lit Framework
Web Components Lit framework development has matured into a first-class approach for building reusable, framework-agnostic UI libraries. Lit 4.0, released in early 2026, brings significant performance improvements, enhanced SSR support, and a developer experience that rivals React and Vue — while producing components that work everywhere HTML works.
This guide covers building a production design system with Lit, from individual components to theming infrastructure to distribution. Moreover, you will learn how Web Components integrate seamlessly into React, Angular, Vue, and vanilla HTML applications without wrapper libraries or compatibility layers.
Why Web Components in 2026
The frontend framework landscape continues to fragment. React 19, Vue 3, Angular 19, Svelte 5, and Solid.js all compete for developer attention. If your organization builds shared UI components, committing to any single framework means those components cannot be used by teams on different stacks. Additionally, framework migrations (which happen every 3-5 years) require rewriting your entire component library.
Web Components solve this by building on browser-native standards: Custom Elements, Shadow DOM, and HTML Templates. A Lit component is a standard Custom Element that works in any environment — including server-rendered pages, WordPress sites, and legacy jQuery applications.
Web Components Lit: Building Your First Component
# Initialize a Lit project
npm create lit@latest my-design-system
cd my-design-system
npm install// src/components/ds-button.ts
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@customElement('ds-button')
export class DsButton extends LitElement {
static styles = css`
:host {
display: inline-flex;
}
:host([hidden]) {
display: none;
}
button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: var(--ds-button-padding, 10px 20px);
border: none;
border-radius: var(--ds-radius-md, 8px);
font-family: var(--ds-font-family, system-ui);
font-size: var(--ds-font-size-md, 14px);
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button.primary {
background: var(--ds-color-primary, #3b82f6);
color: white;
}
button.primary:hover {
background: var(--ds-color-primary-hover, #2563eb);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
button.secondary {
background: transparent;
color: var(--ds-color-primary, #3b82f6);
border: 2px solid currentColor;
}
button.danger {
background: var(--ds-color-danger, #ef4444);
color: white;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
button.loading {
position: relative;
color: transparent;
}
.spinner {
position: absolute;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
`;
@property({ type: String }) variant: 'primary' | 'secondary' | 'danger' = 'primary';
@property({ type: Boolean }) disabled = false;
@property({ type: Boolean }) loading = false;
@property({ type: String }) size: 'sm' | 'md' | 'lg' = 'md';
render() {
const classes = {
[this.variant]: true,
loading: this.loading,
};
return html`
<button
class=${classMap(classes)}
?disabled=${this.disabled || this.loading}
@click=${this._handleClick}
>
${this.loading ? html`<span class="spinner"></span>` : ''}
<slot></slot>
</button>
`;
}
private _handleClick(e: Event) {
if (this.loading || this.disabled) {
e.stopPropagation();
return;
}
this.dispatchEvent(new CustomEvent('ds-click', {
bubbles: true,
composed: true,
detail: { originalEvent: e }
}));
}
}Theming with CSS Custom Properties
Therefore, a robust theming system is essential for any design system. Lit components use CSS Custom Properties (CSS variables) which pierce through Shadow DOM boundaries, allowing consumers to customize appearance without modifying component internals.
// src/themes/tokens.ts
export const lightTheme = css`
:root {
/* Colors */
--ds-color-primary: #3b82f6;
--ds-color-primary-hover: #2563eb;
--ds-color-danger: #ef4444;
--ds-color-success: #22c55e;
--ds-color-warning: #f59e0b;
--ds-color-bg: #ffffff;
--ds-color-surface: #f8fafc;
--ds-color-text: #0f172a;
--ds-color-text-muted: #64748b;
--ds-color-border: #e2e8f0;
/* Typography */
--ds-font-family: 'Inter', system-ui, sans-serif;
--ds-font-size-sm: 12px;
--ds-font-size-md: 14px;
--ds-font-size-lg: 16px;
--ds-font-size-xl: 20px;
/* Spacing */
--ds-space-xs: 4px;
--ds-space-sm: 8px;
--ds-space-md: 16px;
--ds-space-lg: 24px;
--ds-space-xl: 32px;
/* Radius */
--ds-radius-sm: 4px;
--ds-radius-md: 8px;
--ds-radius-lg: 12px;
--ds-radius-full: 9999px;
/* Shadows */
--ds-shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--ds-shadow-md: 0 4px 6px rgba(0,0,0,0.07);
--ds-shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
}
`;
export const darkTheme = css`
:root[data-theme="dark"] {
--ds-color-primary: #60a5fa;
--ds-color-primary-hover: #93bbfd;
--ds-color-bg: #0f172a;
--ds-color-surface: #1e293b;
--ds-color-text: #f1f5f9;
--ds-color-text-muted: #94a3b8;
--ds-color-border: #334155;
}
`;Complex Component Patterns
Consequently, real design systems need complex interactive components like data tables, modals, and form controls. Lit handles these with reactive controllers and context protocol for state sharing.
// src/components/ds-data-table.ts
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
render?: (value: any, row: T) => unknown;
}
@customElement('ds-data-table')
export class DsDataTable<T extends Record<string, any>> extends LitElement {
@property({ type: Array }) columns: Column<T>[] = [];
@property({ type: Array }) data: T[] = [];
@property({ type: Boolean }) selectable = false;
@state() private _sortColumn: string | null = null;
@state() private _sortDirection: 'asc' | 'desc' = 'asc';
@state() private _selectedRows: Set<number> = new Set();
private get sortedData(): T[] {
if (!this._sortColumn) return this.data;
return [...this.data].sort((a, b) => {
const aVal = a[this._sortColumn!];
const bVal = b[this._sortColumn!];
const cmp = String(aVal).localeCompare(String(bVal));
return this._sortDirection === 'asc' ? cmp : -cmp;
});
}
private _toggleSort(column: string) {
if (this._sortColumn === column) {
this._sortDirection = this._sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this._sortColumn = column;
this._sortDirection = 'asc';
}
}
render() {
return html`
<table>
<thead>
<tr>
${this.columns.map(col => html`
<th @click=${col.sortable ? () => this._toggleSort(String(col.key)) : null}>
${col.label}
${col.sortable && this._sortColumn === col.key
? this._sortDirection === 'asc' ? '↑' : '↓' : ''}
</th>
`)}
</tr>
</thead>
<tbody>
${repeat(this.sortedData, (_, i) => i, (row, index) => html`
<tr class=${this._selectedRows.has(index) ? 'selected' : ''}>
${this.columns.map(col => html`
<td>${col.render ? col.render(row[col.key], row) : row[col.key]}</td>
`)}
</tr>
`)}
</tbody>
</table>
`;
}
}When NOT to Use Web Components
If your entire organization uses a single framework like React and has no plans to change, building with Web Components adds an abstraction layer without clear benefit. React’s component model with JSX is more ergonomic than Lit for React-only teams. Additionally, Web Components have limitations with server-side rendering — while Lit’s SSR story has improved significantly, it still requires extra infrastructure compared to framework-native SSR.
Form integration can be challenging with Shadow DOM. Native form controls inside shadow roots do not participate in parent forms by default, requiring the ElementInternals API. This works in modern browsers but adds complexity.
Key Takeaways
Web Components Lit framework development enables truly reusable UI components that work across any web technology. The 2026 ecosystem offers mature tooling, excellent performance, and broad browser support. Furthermore, building your design system on web standards protects your investment from framework churn.
Start with 3-5 foundational components (button, input, card, modal, alert) and expand based on team needs. For more details, see the Lit documentation and Open Web Components recommendations. Our guides on Tailwind CSS 4 features and Astro 5 for static sites offer complementary frontend approaches.