Skip to content

Why Lit

In a vanilla Custom Element, every time state changes you must manually update the DOM. Here is a counter built without Lit:

class VanillaCounter extends HTMLElement {
constructor() {
super();
this._count = 0;
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this._render();
}
_render() {
this.shadowRoot.innerHTML =
'<p>Count: <b>' + this._count + '</b></p>' +
'<button id="inc">+</button>' +
'<button id="dec">-</button>';
this.shadowRoot.querySelector('#inc').addEventListener('click', () => {
this._count++;
this._render();
});
this.shadowRoot.querySelector('#dec').addEventListener('click', () => {
this._count--;
this._render();
});
}
}
customElements.define('vanilla-counter', VanillaCounter);

This approach has several pain points:

  • innerHTML re-creates the whole subtree on every update — event listeners are re-added each time.
  • No declarative binding — every update requires imperative DOM surgery.
  • No property observation — you must wire attributeChangedCallback manually.

Lit solves these pain points with three features:

  • static properties — declares reactive properties; changing one schedules an efficient re-render.
  • html`...` tagged template — a declarative template that diffs and patches only the changed parts of the DOM.
  • static styles with css`...` — styles are adopted once into the Shadow DOM stylesheet, not re-parsed on each render.

Lit is not always necessary. Use plain HTMLElement when:

  • The element is purely structural (a layout wrapper with no state).
  • You are writing a one-off widget with no dynamic updates.
  • Bundle size is the primary constraint and the element does almost nothing.

Use Lit when state changes need to drive UI updates, when you have multiple reactive properties, or when you are building a component library.

The Lit counter — same behaviour, far less code

Section titled “The Lit counter — same behaviour, far less code”
OptionBenefitCost
Use LitElement (Lit)Much better developer experience from reactive properties and template diffing, so you don’t write DOM updates by handAdds roughly +5-6KB (gzip) of runtime to every component’s bundle
Use raw HTMLElement (no library)No added runtime at all, and relies purely on web standards, giving it the longest possible shelf lifeYou have to write all the boilerplate yourself — attribute parsing, dirty-checking, and DOM diffing by hand
  • Choosing Lit when the element has no state at all — if a component is purely static structure, pulling in Lit adds +5-6KB for no benefit. A bare HTMLElement is enough.
  • Worrying that Lit “locks you in” long term — LitElement compiles down to a plain standard custom element; it doesn’t lock in a runtime the way a typical framework does, so migrating away later isn’t as hard as it might seem.
  • Hand-rolling your own diffing when state gets complex — managing re-renders by hand once you have several pieces of state usually ends up heavier and buggier than using Lit’s well-tested template diffing.

💡 Real-world examples

Google Material Web — Google’s standard Material Design component set is built with Lit and used in production across several Google products. (material-components/material-web)

Home Assistant — this popular open-source home automation platform builds its entire frontend UI with Lit-based custom elements. (home-assistant/frontend)

What is a key problem with using innerHTML for re-renders in vanilla Custom Elements?
Which Lit feature replaces manual DOM updates with efficient diffing?
When is it appropriate to skip Lit and use a plain HTMLElement?