Lit vs FAST vs Stencil: Web Component Frameworks in 2026
Lit vs FAST vs Stencil: Web Component Frameworks in 2026
TL;DR
Web components (Custom Elements + Shadow DOM) are the framework-agnostic way to build UI. Lit (Google) is the minimal, performant choice — a ~5 KB library that adds reactive properties and a templating system on top of native Custom Elements. FAST (Microsoft) is the enterprise design system toolkit — @microsoft/fast-element is comparable to Lit in size, but FAST's ecosystem includes @microsoft/fast-components, the reference implementation of Microsoft's Fluent design system. Stencil is the compiler approach — it produces framework-agnostic web components but also generates React, Vue, and Angular wrappers automatically. For lean, framework-agnostic components: Lit. For Microsoft Fluent ecosystem integration: FAST. For generating components that work across every framework with native wrappers: Stencil.
Key Takeaways
- Lit GitHub stars: ~18k — the most widely adopted web component library
- Lit's bundle size: ~5 KB gzipped — minimal overhead vs raw Custom Elements API
- Stencil generates framework wrappers — outputs React, Vue, and Angular bindings automatically from one codebase
- FAST powers the FluentUI Web Components — used in Microsoft 365, Teams, and Edge
- All three use TypeScript decorators —
@customElement,@property,@statepatterns are similar - Stencil's compiler enables SSR — pre-rendering for SEO without a framework
- Web components work in React 19 — React finally added full web component support in React 19
Why Web Components in 2026?
Web components solve a specific problem: framework-agnostic reusability. A Lit component works in React, Vue, Angular, Svelte, and vanilla HTML without modification. This makes web components the right choice for:
- Design systems used across multiple frameworks (enterprise teams)
- Third-party widgets embedded in customer pages
- Microfrontend architectures where teams use different frameworks
- Library authors who can't predict what framework consumers use
Lit: Google's Web Component Library
Lit is Google's official web component library, used in Google's own products and the base for many design systems. It extends the browser's Custom Elements API with reactive properties and tagged template literals.
Installation
npm install lit
Basic Component
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
@customElement("my-button")
export class MyButton extends LitElement {
// Reflected attribute + reactive property
@property({ type: String }) label = "Click me";
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: String }) variant: "primary" | "secondary" = "primary";
// Internal state (doesn't reflect to attribute)
@state() private _clicks = 0;
static styles = css`
:host {
display: inline-block;
}
button {
padding: 8px 16px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: opacity 0.15s;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
:host([variant="primary"]) button {
background: #3b82f6;
color: white;
}
:host([variant="secondary"]) button {
background: #f3f4f6;
color: #374151;
}
`;
render() {
return html`
<button
?disabled=${this.disabled}
@click=${this._handleClick}
>
${this.label} (${this._clicks})
</button>
`;
}
private _handleClick() {
this._clicks++;
this.dispatchEvent(
new CustomEvent("my-click", {
detail: { clicks: this._clicks },
bubbles: true,
composed: true, // Cross shadow DOM boundary
})
);
}
}
Reactive Properties and Updates
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
@customElement("user-card")
export class UserCard extends LitElement {
@property({ type: String }) userId = "";
@state() private _user: { name: string; email: string } | null = null;
@state() private _loading = false;
// willUpdate runs when properties change before re-render
willUpdate(changedProperties: Map<string, unknown>) {
if (changedProperties.has("userId") && this.userId) {
this._fetchUser();
}
}
private async _fetchUser() {
this._loading = true;
const response = await fetch(`/api/users/${this.userId}`);
this._user = await response.json();
this._loading = false;
}
render() {
if (this._loading) return html`<p>Loading...</p>`;
if (!this._user) return html`<p>No user</p>`;
return html`
<div class="card">
<h2>${this._user.name}</h2>
<p>${this._user.email}</p>
</div>
`;
}
}
Slots and Composition
@customElement("lit-card")
export class LitCard extends LitElement {
static styles = css`
.card { border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.header { padding: 16px; background: #f9fafb; border-bottom: 1px solid #e5e7eb; }
.body { padding: 16px; }
.footer { padding: 12px 16px; background: #f9fafb; border-top: 1px solid #e5e7eb; }
`;
render() {
return html`
<div class="card">
<div class="header">
<slot name="header">Default Header</slot>
</div>
<div class="body">
<slot></slot> <!-- Default slot -->
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`;
}
}
<!-- Usage in any HTML/framework -->
<lit-card>
<span slot="header">My Card Title</span>
<p>Card body content goes here</p>
<button slot="footer">Action</button>
</lit-card>
FAST: Microsoft's Enterprise Web Component Foundation
FAST (@microsoft/fast-element) is the foundational layer for Microsoft's Fluent design system. It's comparable to Lit in philosophy but uses different template syntax and is deeply integrated with Microsoft's design token system.
Installation
npm install @microsoft/fast-element
Basic FAST Element
import {
FASTElement,
customElement,
attr,
observable,
html,
css,
when,
} from "@microsoft/fast-element";
const template = html<FastButton>`
<button
?disabled="${(x) => x.disabled}"
@click="${(x, c) => x.handleClick(c.event as MouseEvent)}"
>
${when(
(x) => x.loading,
html`<span>Loading...</span>`,
html`${(x) => x.label}`
)}
</button>
`;
const styles = css`
:host { display: inline-block; }
button {
background: ${neutralFillRest}; /* FAST design token */
color: ${neutralForegroundRest};
border: 1px solid ${neutralStrokeRest};
padding: 8px 16px;
border-radius: ${controlCornerRadius};
cursor: pointer;
}
`;
@customElement({ name: "fast-button", template, styles })
export class FastButton extends FASTElement {
@attr label = "Button";
@attr({ mode: "boolean" }) disabled = false;
@attr({ mode: "boolean" }) loading = false;
handleClick(event: MouseEvent) {
if (!this.disabled && !this.loading) {
this.$emit("button-click", { event });
}
}
}
FAST Design Tokens
import {
DesignToken,
DesignTokenChangeRecord,
} from "@microsoft/fast-foundation";
// Define custom design tokens
export const brandColor = DesignToken.create<string>("brand-color").withDefault(
"#3b82f6"
);
export const brandColorLight = DesignToken.create<string>(
"brand-color-light"
).withDefault("#dbeafe");
// Apply tokens to a subtree
const myElement = document.getElementById("app");
brandColor.setValueFor(myElement, "#6366f1"); // Override for this subtree
// Use in CSS
const styles = css`
:host { background: ${brandColor}; }
`;
Fluent UI Web Components (Using FAST)
<!-- Microsoft's production FAST components -->
<script type="module">
import { provideFluentDesignSystem, fluentButton, fluentCard } from
"https://unpkg.com/@fluentui/web-components";
provideFluentDesignSystem().register(fluentButton(), fluentCard());
</script>
<!-- Use Fluent components anywhere -->
<fluent-card>
<h2>Fluent Card</h2>
<p>Powered by FAST and Microsoft's Fluent design system</p>
<fluent-button appearance="accent">Primary Action</fluent-button>
<fluent-button appearance="neutral">Secondary</fluent-button>
</fluent-card>
Stencil: The Compiler for Framework-Agnostic Components
Stencil (from Ionic team) is a compiler, not a runtime library. It compiles TypeScript + JSX into standard web components AND optionally generates React, Vue, and Angular wrappers — meaning your component works natively in each framework.
Installation
npm init stencil component my-components
cd my-components
npm install
Basic Stencil Component
import { Component, Prop, State, Event, EventEmitter, h } from "@stencil/core";
@Component({
tag: "my-input",
styleUrl: "my-input.css",
shadow: true, // Enable Shadow DOM
})
export class MyInput {
// Props from outside
@Prop() label: string = "Input";
@Prop() placeholder: string = "";
@Prop() value: string = "";
@Prop({ mutable: true }) disabled: boolean = false;
// Internal state
@State() private focused: boolean = false;
// Custom events
@Event() myChange: EventEmitter<string>;
@Event() myFocus: EventEmitter<void>;
handleInput(event: Event) {
const input = event.target as HTMLInputElement;
this.myChange.emit(input.value);
}
render() {
return (
<div class={{ "input-wrapper": true, "focused": this.focused }}>
{this.label && <label>{this.label}</label>}
<input
type="text"
placeholder={this.placeholder}
value={this.value}
disabled={this.disabled}
onInput={(e) => this.handleInput(e)}
onFocus={() => { this.focused = true; this.myFocus.emit(); }}
onBlur={() => { this.focused = false; }}
/>
</div>
);
}
}
Stencil Framework Wrapper Generation
# Build the component library with framework outputs
npx stencil build
// stencil.config.ts — configure output targets
import { Config } from "@stencil/core";
import { reactOutputTarget } from "@stencil/react-output-target";
import { vueOutputTarget } from "@stencil/vue-output-target";
import { angularOutputTarget } from "@stencil/angular-output-target";
export const config: Config = {
namespace: "my-components",
outputTargets: [
// Web component (standard)
{ type: "dist", esmLoaderPath: "../loader" },
// React wrapper (auto-generated)
reactOutputTarget({
componentCorePackage: "my-components",
proxiesFile: "../my-components-react/src/components.ts",
includeDefineCustomElements: true,
}),
// Vue wrapper (auto-generated)
vueOutputTarget({
componentCorePackage: "my-components",
proxiesFile: "../my-components-vue/src/components.ts",
}),
// Angular wrapper (auto-generated)
angularOutputTarget({
componentCorePackage: "my-components",
directivesProxyFile: "../my-components-angular/src/directives/proxies.ts",
}),
],
};
// After build: use in React with full type safety
import { MyInput } from "my-components-react";
function ReactApp() {
return (
<MyInput
label="Email"
placeholder="Enter email..."
onMyChange={(e) => console.log(e.detail)}
/>
);
}
Stencil SSR / Prerendering
// stencil.config.ts — enable pre-rendering for SEO
export const config: Config = {
outputTargets: [
{
type: "www",
serviceWorker: null,
baseUrl: "https://myapp.com",
prerenderConfig: "./prerender.config.ts",
},
],
};
// prerender.config.ts
import { PrerenderConfig } from "@stencil/core";
export const config: PrerenderConfig = {
crawlUrls: true, // Auto-discover URLs via links
routes: ["/", "/about", "/products"],
afterHydrate(document: Document, url: URL) {
// Modify the document after hydration if needed
document.title = `${document.title} | My App`;
},
};
Feature Comparison
| Feature | Lit | FAST | Stencil |
|---|---|---|---|
| Type | Library (~5 KB) | Library (~7 KB) | Compiler |
| Template syntax | Tagged template literals | HTML html tag | JSX |
| TypeScript decorators | ✅ | ✅ | ✅ |
| Shadow DOM | Optional | Optional | Optional |
| Design tokens | Theming CSS | ✅ FAST tokens | ✅ CSS custom props |
| React wrapper output | Manual | Manual | ✅ Auto-generated |
| Vue wrapper output | Manual | Manual | ✅ Auto-generated |
| Angular wrapper output | Manual | Manual | ✅ Auto-generated |
| SSR / Pre-rendering | Via @lit-labs/ssr | ❌ | ✅ Built-in |
| Component library | Shoelace, Lion | Fluent UI Web | Ionic |
| Storybook support | ✅ | ✅ | ✅ |
| Bundle size (runtime) | ~5 KB | ~7 KB | 0 (compile-time) |
| GitHub stars | 18k | 9k | 12k |
| Backed by | Microsoft | Ionic | |
| Learning curve | Low | Medium | Medium |
| Use case fit | Standalone components | Microsoft ecosystem | Multi-framework design systems |
When to Use Each
Choose Lit if:
- You want the leanest runtime (~5 KB) for standalone web components
- Template literals feel natural and you don't want JSX
- Your team is building a design system for diverse framework consumers
- You want close alignment with web standards (no magic compiler)
Choose FAST if:
- You're building within the Microsoft 365 / Teams / Azure ecosystem
- You want to use or extend the Fluent design system
- FAST's design token system matches your design infrastructure
- You need deep enterprise-grade component composition patterns
Choose Stencil if:
- You need to publish a component library usable natively in React, Vue, AND Angular
- SSR/pre-rendering for SEO is required without a full framework
- Your team knows JSX and wants familiar syntax
- You're building on top of Ionic or extending the Ionic ecosystem
Methodology
Data sourced from GitHub repositories (star counts as of February 2026), npm weekly download statistics (January 2026), official documentation, and community discussions on the Lit Discord, FAST GitHub discussions, and StencilJS Discord. Bundle sizes measured from bundlephobia.com. Feature verification against documentation.
Related: React vs Svelte vs Solid 2026 for full framework comparisons, or shadcn/ui vs Radix UI for React-specific component approaches.