Back to blog
Tutorials

Lit + TypeScript: Web Components the Way They Should Be

Kim BoenderKim Boender
March 30, 2026 7 min read
Lit + TypeScript: Web Components the Way They Should Be

Have you ever looked at the raw Web Components API and thought "this should be better"? You're not wrong. The browser's native component model has been around for years, but the developer experience around it was genuinely painful for a long time. That changed when Lit arrived.

Combine Lit with TypeScript, and you get something genuinely interesting: framework-agnostic, standards-based components with the type safety and developer ergonomics modern teams expect. Let me walk you through what Lit is, why it matters, and how TypeScript makes it shine.

What is Lit?

Lit is a lightweight library from Google that makes building Web Components fast and enjoyable. It's built on top of native browser APIs, Custom Elements, Shadow DOM, and HTML Templates, which means components built with Lit work anywhere HTML works. No framework dependency. No virtual DOM overhead. Just the platform.

The library itself is tiny: under 6KB minified and gzipped. And because it compiles to standard Web Components, you can drop a Lit component into a Vue app, a React app, a plain HTML page, or a server-rendered document. That's something no framework component can claim.

Setting up Lit with TypeScript

Getting started is straightforward. Install the packages:

npm install lit
npm install --save-dev typescript

A minimal tsconfig.json for Lit looks like this:

{
  "compilerOptions": {
    "target": "ES2021",
    "module": "ES2020",
    "moduleResolution": "node",
    "strict": true,
    "experimentalDecorators": true,
    "useDefineForClassFields": false,
    "lib": ["ES2021", "DOM", "DOM.Iterable"]
  }
}

Note experimentalDecorators: true. Lit uses decorators heavily, and they're what make the authoring experience so clean.

Your first Lit component in TypeScript

Here's a simple counter component:

import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

@customElement('my-counter')
export class MyCounter extends LitElement {
  static styles = css`
    :host { display: block; font-family: sans-serif; }
    button { padding: 0.5rem 1rem; font-size: 1rem; cursor: pointer; }
    p { font-size: 1.25rem; }
  `;

  @state() private count = 0;

  render() {
    return html`
      <p>Count: ${this.count}</p>
      <button @click=${this._increment}>Increment</button>
    `;
  }

  private _increment() {
    this.count++;
  }
}

Drop <my-counter></my-counter> into any HTML page, and it works. No build pipeline required for consumption. No framework runtime on the receiving end.

Decorators: the TypeScript sweet spot

Lit's decorator API is where TypeScript really earns its keep. Here are the key ones.

@customElement registers your class as a custom element. TypeScript will enforce that you've extended LitElement correctly.

@property declares a public reactive property that triggers a re-render when changed and can be set as an HTML attribute. With TypeScript, you get full type inference on these. If you set name to a number somewhere, the compiler catches it immediately.

@state declares private reactive state. Same reactivity as @property, but not exposed to the outside world.

@query is a convenience decorator for querying shadow DOM elements: typed, clean, no manual shadowRoot.querySelector calls.

Typed props and events: building a real component

Here's a more realistic example: a typed card component that emits custom events:

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

export interface CardSelectEvent {
  id: string;
  title: string;
}

@customElement('project-card')
export class ProjectCard extends LitElement {
  static styles = css`
    :host {
      display: block;
      border: 1px solid #e2e8f0;
      border-radius: 8px;
      padding: 1.5rem;
      cursor: pointer;
      transition: box-shadow 0.2s;
    }
    :host(:hover) { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
    h2 { margin: 0 0 0.5rem; }
    p { margin: 0; color: #64748b; }
  `;

  @property({ type: String }) cardId = '';
  @property({ type: String }) title = '';
  @property({ type: String }) description = '';

  render() {
    return html`
      <h2>${this.title}</h2>
      <p>${this.description}</p>
    `;
  }

  connectedCallback() {
    super.connectedCallback();
    this.addEventListener('click', this._handleClick);
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.removeEventListener('click', this._handleClick);
  }

  private _handleClick = () => {
    const event = new CustomEvent<CardSelectEvent>('card-select', {
      detail: { id: this.cardId, title: this.title },
      bubbles: true,
      composed: true,
    });
    this.dispatchEvent(event);
  };
}

The composed: true flag is important. It lets the event bubble out of the Shadow DOM boundary so parent frameworks can listen for it normally.

Shadow DOM and encapsulation

One of Lit's most powerful features is Shadow DOM encapsulation. Styles defined in a Lit component are scoped to that component by default. No CSS leaks in, no CSS leaks out.

This is a real win if you've ever dealt with stylesheet conflicts in large applications. Your button styles won't affect anything outside the component, and external global styles won't accidentally override your internals.

You can still allow controlled styling from the outside using CSS custom properties:

static styles = css`
  :host {
    --btn-bg: #3b82f6;
    --btn-color: white;
  }
  button {
    background: var(--btn-bg);
    color: var(--btn-color);
  }
`;

Consumers can then override those variables without needing to know anything about your internal markup.

When would I actually use Lit?

As someone who primarily works in Vue and React, Lit isn't my first choice for a full application. But there are real scenarios where it shines.

Design systems and component libraries are the obvious one. If you're building a component library that needs to work across multiple frameworks, or you want to stop rewriting the same button component in Vue, React, and Angular, Lit gives you one implementation that works everywhere.

Micro-frontends are another good fit. When different teams own different parts of an application and use different stacks, Web Components are a natural boundary. Lit makes those components production-quality.

Embedding widgets in third-party contexts also benefits enormously from Shadow DOM encapsulation and zero framework requirements. Marketing pages, embeddable tools, and dashboard widgets that need to be dropped into arbitrary environments are natural use cases.

Progressive enhancement on existing sites is another good fit. Lit components load fast, work without a JavaScript framework, and degrade gracefully.

Lit vs the frameworks: an honest take

Lit is not a replacement for Vue or React. It doesn't have routing, doesn't have a full-featured state management story, and its ecosystem is much smaller. If you're building a complex SPA, Vue 3 or Next.js will serve you better.

But Lit isn't trying to be those things. It's trying to be the best way to build Web Components, and at that, it succeeds. The fact that those components then work in any framework, in any context, with no runtime overhead? That's a genuine superpower.

For those of us who care about the long-term health of the web, there's also something satisfying about using the platform rather than papering over it. Web Components are a standard. Lit components today will still work in browsers ten years from now, long after the framework du jour has been replaced by something else.

Getting started

The official Lit docs at lit.dev are excellent and well-maintained. There's a Playground built right into the site where you can experiment with components live before setting up a local environment.

For a TypeScript-ready starter, the Lit team maintains project starters via npm init @open-wc. It scaffolds a full Lit + TypeScript project with testing, linting, and a dev server out of the box.

npm init @open-wc

Choose "Web Component" and "TypeScript", and you're up and running in minutes.

Final thoughts

Lit with TypeScript hits a sweet spot that not many libraries do: it's small, standards-based, and genuinely pleasant to work with. The decorator API feels natural to anyone coming from Angular or modern TypeScript, the Shadow DOM encapsulation solves real problems, and the ability to ship components that work everywhere is uniquely valuable.

If you've written off Web Components because of the raw API, give Lit another look. It's the library that makes the browser's native component model actually usable.

Frequently Asked Questions

What is Lit and how is it different from React or Vue? +
Lit is a library for building Web Components — native browser components that work without any framework. Unlike React or Vue, which are full application frameworks with their own rendering engines and ecosystems, Lit is a thin layer on top of the browser's built-in Custom Elements and Shadow DOM APIs. A Lit component can be dropped into a React app, a Vue app, or a plain HTML file. React and Vue components can't do that without wrappers.
Do I need to know Web Components to use Lit? +
Not deeply, but a basic understanding helps. Lit abstracts most of the raw Web Components API behind decorators and a clean reactive system, so you won't be writing `customElements.define()` or manually managing Shadow DOM yourself. That said, knowing the basics of Custom Elements and Shadow DOM will help you understand what's happening under the hood, especially when it comes to event composition and CSS encapsulation.
Can I use Lit components inside a React or Vue application? +
Yes — that's one of Lit's biggest selling points. Because Lit components are standard Web Components, you can import and use them in React, Vue, Angular, Svelte, or plain HTML without any special adapters. In React, you use them like any custom HTML element. In Vue, you may need to configure `compilerOptions.isCustomElement` to suppress unknown element warnings. The custom events Lit emits (with `composed: true`) bubble correctly through framework boundaries.
Is Lit a good choice for building a full application? +
It depends on the application. For design systems, component libraries, embeddable widgets, or micro-frontends, Lit is an excellent choice. For a full SPA with routing, complex state management, and a large team, Vue 3 or Next.js will serve you better — their ecosystems, tooling, and communities are more mature. That said, Google has built large applications with Lit (including parts of their own products), so it's not off the table if your team is committed to the platform-native approach.

Try it yourself

JSON Formatter

Format, validate, and beautify JSON instantly

Open JSON Formatter