Skip to main content

Command Palette

Search for a command to run...

Lit: Web Components That Actually Work

Learn: Lit: Web Components That Actually Work

Updated
5 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Lit: Web Components That Actually Work

Standards-based UI development is finally ready for production

The frontend landscape has been dominated by React, Vue, and Angular for years. But there's a quiet revolution happening: Lit is making Web Components—the browser's native component model—actually enjoyable to use. No virtual DOM. No framework lock-in. Just standards-based components that work everywhere.

Why Frontend is Changing Again

We've been here before. jQuery gave way to Angular, which competed with React, which spawned countless alternatives. Each cycle promised to solve the problems of the last, but introduced new complexity. Bundle sizes grew. Build tools multiplied. Framework fatigue became real.

The irony? Browsers have supported a native component model since 2016. Web Components let you create reusable, encapsulated HTML elements using standards: Custom Elements, Shadow DOM, and HTML Templates. The problem was always developer experience—too verbose, too low-level, too painful.

Lit changes this equation. It's a thin layer (just 5KB) over Web Components that adds reactivity, templating, and modern conveniences while staying true to the platform. Your components are real DOM elements that work in any framework—or no framework at all.

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

@customElement('user-card')
export class UserCard extends LitElement {
  @property() name = '';
  @property({ type: Number }) age = 0;

  static styles = css`
    :host {
      display: block;
      padding: 16px;
      border: 2px solid #333;
      border-radius: 8px;
    }
    h2 { margin: 0 0 8px 0; }
  `;

  render() {
    return html`
      <h2>${this.name}</h2>
      <p>Age: ${this.age}</p>
    `;
  }
}

Use it anywhere—React, Vue, vanilla HTML:

<user-card name="Sarah" age="28"></user-card>

The Core Innovation

Lit's genius is in what it doesn't do. There's no virtual DOM diffing, no complex reconciliation algorithm, no framework-specific concepts to learn. Instead, Lit leverages three key innovations:

1. Tagged Template Literals for Templating

JavaScript's native template literals become your templating language. No JSX compilation, no new syntax:

render() {
  const items = ['Apple', 'Banana', 'Cherry'];
  return html`
    <ul>
      ${items.map(item => html`<li>${item}</li>`)}
    </ul>
  `;
}

2. Surgical DOM Updates

Lit analyzes your templates once and creates optimized update functions. When data changes, only the specific text nodes or attributes that depend on that data get updated:

@property() count = 0;

render() {
  return html`
    <button @click=${this._increment}>
      Clicked ${this.count} times
    </button>
  `;
}

_increment() {
  this.count++;
  // Lit automatically updates ONLY the text node with count
}

3. Reactive Properties

Decorators (or static properties) define reactive state. Change a property, and Lit schedules an efficient re-render:

@property({ type: Boolean }) 
loading = false;

async fetchData() {
  this.loading = true;
  const data = await fetch('/api/data');
  this.data = await data.json();
  this.loading = false; // Triggers update
}

How It Works

Under the hood, Lit is remarkably simple. When you define a component:

  1. Registration: Your class extends LitElement, which extends HTMLElement. The @customElement decorator registers it with the browser's Custom Elements registry.

  2. Template Processing: The first time render() runs, Lit parses your template literal and identifies dynamic parts (the ${...} expressions).

  3. Efficient Updates: Lit creates a template element and clones it for each instance. It remembers where the dynamic parts are and updates only those locations when properties change.

  4. Shadow DOM Encapsulation: Styles are scoped to your component automatically. No CSS-in-JS library needed, no class name collisions:

@customElement('todo-item')
export class TodoItem extends LitElement {
  @property({ type: Boolean }) completed = false;
  @property() text = '';

  static styles = css`
    :host {
      display: flex;
      padding: 8px;
      background: var(--item-bg, white);
    }
    :host([completed]) {
      opacity: 0.6;
      text-decoration: line-through;
    }
    button {
      margin-left: auto;
    }
  `;

  render() {
    return html`
      <input 
        type="checkbox" 
        .checked=${this.completed}
        @change=${this._toggleComplete}
      >
      <span>${this.text}</span>
      <button @click=${this._delete}>Delete</button>
    `;
  }

  _toggleComplete() {
    this.completed = !this.completed;
    this.dispatchEvent(new CustomEvent('toggle', { 
      detail: { completed: this.completed }
    }));
  }

  _delete() {
    this.dispatchEvent(new Event('delete'));
  }
}

Building Your First App

Let's build a real-world feature: a filterable product list with cart functionality.

@customElement('product-list')
export class ProductList extends LitElement {
  @property({ type: Array }) products = [];
  @property() filter = '';
  @state() cart = [];

  static styles = css`
    .filters { margin-bottom: 16px; }
    .products { 
      display: grid; 
      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
      gap: 16px;
    }
    .cart-count {
      position: fixed;
      top: 16px;
      right: 16px;
      background: #007bff;
      color: white;
      padding: 8px 16px;
      border-radius: 20px;
    }
  `;

  render() {
    const filtered = this.products.filter(p => 
      p.name.toLowerCase().includes(this.filter.toLowerCase())
    );

    return html`
      <div class="cart-count">Cart: ${this.cart.length}</div>

      <div class="filters">
        <input 
          type="search"
          placeholder="Filter products..."
          .value=${this.filter}
          @input=${e => this.filter = e.target.value}
        >
      </div>

      <div class="products">
        ${filtered.map(product => html`
          <product-card
            .product=${product}
            @add-to-cart=${e => this._addToCart(e.detail)}
          ></product-card>
        `)}
      </div>
    `;
  }

  _addToCart(product) {
    this.cart = [...this.cart, product];
  }
}

Notice: no build step required for development. Import from a CDN and you're running.

Performance Benefits

Lit's performance characteristics are compelling:

Bundle Size: 5KB minified + gzipped. React is 42KB. Vue is 34KB. Your users download less.

Runtime Speed: No virtual DOM means no diffing overhead. Updates are as fast as direct DOM manipulation because that's exactly what they are.

Memory Efficiency: Each component instance is a real DOM element. No parallel virtual tree consuming memory.

Lazy Loading: Web Components are perfect for code splitting:

// Load heavy components only when needed
if (userIsAdmin) {
  await import('./admin-panel.js');
}

Server-Side Rendering: Lit supports Declarative Shadow DOM for SSR with full hydration:

import { render } from '@lit-labs/ssr';

const result = render(html`<user-card name="Alex"></user-card>`);

When to Use It

Lit excels when:

  • Building design systems that work across multiple frameworks
  • Creating embeddable widgets for third-party sites
  • Developing long-lived applications where framework churn is costly
  • Performance is critical (mobile, low-end devices)
  • You want minimal dependencies and build complexity

Consider alternatives when:

  • You need extensive React ecosystem libraries (though this gap is closing)
  • Your team is deeply invested in another framework
  • You need IE11 support (though polyfills exist)

Real-world adoption: Google uses Lit extensively (YouTube, Google Photos). Adobe's Spectrum Web Components are built with Lit. It's production-ready.

Conclusion

Lit represents a maturation of web development. Instead of reinventing the platform, it embraces and enhances it. Your components are future-proof because they're built on standards that browsers implement natively.

The frontend pendulum is swinging back toward simplicity. Not the simplicity of jQuery—we've learned too much about component architecture to go back—but a new simplicity that leverages the platform's evolution.

Start small: convert one component to Lit. Use it alongside your existing framework. Feel the difference of working with the grain of the web platform instead of against it.

The future of frontend might not be a framework at all. It might just be the web.

npm install lit

Welcome to standards-based UI development.