Why Lit
The vanilla pain point
Section titled “The vanilla pain point”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:
innerHTMLre-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
attributeChangedCallbackmanually.
The Lit solution
Section titled “The Lit solution”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 styleswithcss`...`— styles are adopted once into the Shadow DOM stylesheet, not re-parsed on each render.
When vanilla is enough
Section titled “When vanilla is enough”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.