Skip to content

Lit vs Web Components

Lit is built directly on top of the Web Components standards rather than replacing them. This lesson compares writing a component with the raw platform APIs versus writing the same component with Lit.

Web Components Are a Platform Standard, Lit Is a Library

"Web Components" refers to a set of browser standards — Custom Elements, Shadow DOM, and HTML <template> — that let you define new HTML tags with encapsulated markup and styling, natively, with no library required. Every browser has supported these for years.

Lit is a library that sits on top of those standards. It does not replace Custom Elements or Shadow DOM; it makes them dramatically easier to use by adding declarative templates, a reactive property system, and an efficient rendering algorithm.

// Plain Web Component (no library)
class PlainCounter extends HTMLElement {
  #count = 0;
  connectedCallback() { this.render(); }
  render() {
    this.innerHTML = `<button>Count: ${this.#count}</button>`;
    this.querySelector('button').onclick = () => {
      this.#count++;
      this.render(); // must manually re-render everything
    };
  }
}
customElements.define('plain-counter', PlainCounter);

Without Lit, you write your own re-render logic by hand, and every click re-creates the entire button element and re-attaches its listener.

The Same Component, With Lit

class LitCounter extends LitElement {
  static properties = { count: { type: Number } };
  constructor() { super(); this.count = 0; }
  render() {
    return html`<button @click=${() => this.count++}>Count: ${this.count}</button>`;
  }
}
customElements.define('lit-counter', LitCounter);
  • Lit re-renders automatically whenever count changes — no manual render() call needed.
  • Lit's diffing only updates the text node holding count; the <button> element itself is never recreated.
  • The event listener is attached once via the @click binding and reused across renders.
  • The public API (the custom element tag) is identical either way — consumers can't tell which approach you used.

Web Components vs Lit

What the raw platform gives you versus what Lit adds on top.

Capability Raw Web Components Lit
Define a custom tag customElements.define() Same, or @customElement
Encapsulated styles Manual attachShadow() static styles = css...`
Declarative templates Not built-in html...` tagged templates
Reactive re-rendering Manual, by hand Automatic via @property/@state
Efficient DOM updates Manual diffing required Built-in via lit-html
Attribute/property sync Manual observedAttributes Automatic via property options
Lifecycle hooks connectedCallback, etc. Same hooks, plus updated()/firstUpdated()

What the Platform Gives You for Free

Custom Elements let you register a new HTML tag and hook into its lifecycle (connectedCallback, disconnectedCallback, attributeChangedCallback). Shadow DOM lets you attach an isolated DOM subtree to an element, so its internal markup and CSS don't leak out and page styles don't leak in.

These are genuinely useful on their own, but they are low-level. The platform gives you no help rendering HTML from data, no diffing, and no built-in way to keep an attribute and a JavaScript property in sync — you write all of that yourself.

  • Custom Elements: define new tags and hook into creation/removal/attribute-change events.
  • Shadow DOM: encapsulate markup and CSS so components don't clash with the rest of the page.
  • HTML <template>: define inert markup that can be cloned efficiently — the low-level primitive Lit's templating builds on.

What Lit Adds on Top

Lit's contribution is entirely in the 'how do I update this efficiently and declaratively' layer. LitElement wires up Shadow DOM attachment, batches property changes into a single re-render per update cycle, and uses lit-html to apply only the minimal DOM changes needed — comparing the new template's dynamic values to the previous render, not the whole markup string.

Feature Why It Matters
Reactive properties No manual render() calls scattered through your code
Async, batched updates Multiple property changes in one tick trigger a single re-render
Efficient template diffing Only changed expressions touch the real DOM
Directives (repeat, classMap, ...) Solve common templating problems without hand-written DOM code

Common Mistakes

  • Believing Lit and Web Components are competing technologies rather than a library built directly on the standards.
  • Assuming you must choose between 'plain Web Components' and 'Lit' for an entire codebase — you can mix hand-written and Lit-based custom elements freely.
  • Underestimating how much manual diffing and re-render logic Lit removes, especially as a component's template grows.
  • Forgetting that Lit components remain interoperable with vanilla JavaScript and any framework, exactly like hand-written custom elements.

Key Takeaways

  • Web Components are browser standards: Custom Elements, Shadow DOM, and HTML templates.
  • Lit is a library, not a competing standard — it builds directly on top of those APIs.
  • Lit adds declarative templates, reactive properties, and efficient DOM diffing that the raw platform doesn't provide.
  • A component authored with Lit is, from the outside, indistinguishable from a hand-written custom element.

Pro Tip

If you're ever unsure what a Lit feature is 'really' doing under the hood, try writing the same tiny component without Lit first. The gap between the two versions is exactly what Lit is doing for you.