Skip to main content

Lit vs FAST vs Stencil: Web Component Frameworks in 2026

·PkgPulse Team

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, @state patterns 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

FeatureLitFASTStencil
TypeLibrary (~5 KB)Library (~7 KB)Compiler
Template syntaxTagged template literalsHTML html tagJSX
TypeScript decorators
Shadow DOMOptionalOptionalOptional
Design tokensTheming CSS✅ FAST tokens✅ CSS custom props
React wrapper outputManualManual✅ Auto-generated
Vue wrapper outputManualManual✅ Auto-generated
Angular wrapper outputManualManual✅ Auto-generated
SSR / Pre-renderingVia @lit-labs/ssr✅ Built-in
Component libraryShoelace, LionFluent UI WebIonic
Storybook support
Bundle size (runtime)~5 KB~7 KB0 (compile-time)
GitHub stars18k9k12k
Backed byGoogleMicrosoftIonic
Learning curveLowMediumMedium
Use case fitStandalone componentsMicrosoft ecosystemMulti-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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.