Libraries

Hand-written custom elements repeat the same chores: syncing attributes and properties, re-rendering, escaping values. Libraries automate them and still output standard custom elements.

Lit

Lit 310,034 (https://github.com/lit/lit 21,842 ) (Google, BSD-3-Clause, npm 2,036 install lit, version 3.3.3) adds three things to HTMLElement: reactive properties that schedule a batched update when they change, a render() method returning an html tagged template, and static styles scoped to the shadow root. Only the template parts whose values changed are updated (the template cloning technique of <template> and <slot>). The prebuilt lit-core.min.js bundle used below is about 6 KB gzipped.

A Lit component with a reactive property, scoped styles and a custom eventHTMLLive
<script type="module">
  import { LitElement, html, css }
    from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
  class LikeButton extends LitElement {
    static properties = { count: { type: Number, reflect: true }, label: {} };
    static styles = css`
      button { font: 16px system-ui; padding: 6px 14px; border: 1px solid #1565c0;
               border-radius: 999px; background: #e3f2fd; color: #1565c0; }
      :host([count="0"]) button { background: #fff; }`;
    constructor() { super(); this.count = 0; this.label = 'Like'; }
    render() {
      return html`<button @click=${this.#like}>♥ ${this.label} · ${this.count}</button>`;
    }
    #like() {
      this.count++;
      this.dispatchEvent(new CustomEvent('liked', { detail: this.count, bubbles: true }));
    }
  }
  customElements.define('like-button', LikeButton);
  const b = document.querySelector('#b');
  await b.updateComplete;
  b.renderRoot.querySelector('button').click(); // two clicks for the screenshot
  b.renderRoot.querySelector('button').click();
</script>
<like-button></like-button> <like-button id="b" label="Helpful"></like-button>
Browser output of Listing 2.41
Browser output of 41

Because count has reflect: true, the second element now carries count="2" and the :host([count="0"]) rule no longer matches it. In TypeScript you would write @property({ type: Number }) count = 0; instead of static properties. In plain JavaScript, set defaults in the constructor as shown: a class field would shadow Lit's property accessor and break updates.

Stencil

Stencil 672,850 (https://github.com/stenciljs/core 13,132 ) (Ionic 20,753 , MIT, version 4.45.0) is a compiler, not a runtime library. You write TypeScript classes with decorators and JSX; the build emits standard custom elements, lazy-loaded bundles, type definitions and wrappers for React 7,897 , Angular 42,088 and Vue 5,482 . Ionic Framework's own components are built with it. Create a project with npm init stencil and run npm start.

src/components/star-meter/star-meter.tsx: a Stencil componentTSX
import { Component, Prop, State, Event, EventEmitter, h } from '@stencil/core';
@Component({ tag: 'star-meter', styleUrl: 'star-meter.css', shadow: true })
export class StarMeter {
  @Prop() max = 5;                         // public attribute/property: <star-meter max="10">
  @State() value = 0;                      // internal state: changing it re-renders
  @Event() rated: EventEmitter<number>;    // emits a 'rated' DOM event
  render() {
    return Array.from({ length: this.max }, (_, i) => (
      <button onClick={() => { this.value = i + 1; this.rated.emit(i + 1); }}>
        {i < this.value ? '★' : '☆'}
      </button>
    ));
  }
}

The compiler turns the decorators into observed attributes, accessors and a small virtual DOM renderer. Choose Stencil to publish one design system to several frameworks, and Lit for a small runtime with no mandatory build.

Shoelace and Web Awesome

Shoelace, a popular MIT-licensed set of ready-made components built with Lit, is sunset: its repository is archived at version 2.20.1 and development continues as Web Awesome 360,023 (https://github.com/shoelace-style/webawesome 1,348 ), published by the makers of Font Awesome 693 . Web Awesome 3.0 shipped in October 2025 (3.12.0 at the time of writing), uses the wa- prefix, and is themed through CSS custom properties and ::part(). Its Core components are free and MIT licensed; the paid Pro subscription adds data grids, charts, extra themes and page patterns. To try it, link https://ka-f.webawesome.com/webawesome@3.12.0/styles/themes/default.css 360,023 , load webawesome.loader.js from the same folder as a module script, and write <wa-button variant="brand">Save</wa-button>.

Web component libraries compared (September 2026)
Library What it is License Strengths Weaknesses
Lit 3.3 Base class and templating BSD-3-Clause Tiny, no build needed, SSR (Labs) You build every widget
Stencil 4.45 Compiler for component libraries MIT Framework wrappers, lazy loading Build step, own decorators
Web Awesome 3.12 Ready-made components on Lit MIT Core, paid Pro Polished widgets, theming Advanced widgets cost money
FAST Element 3.0 Microsoft's base class MIT Powers Fluent UI Web Components Smaller community