Skip to content

LitElement

LitElement is the base class at the heart of every Lit component. This lesson explains exactly what it provides on top of the native HTMLElement, and how its reactive update cycle works.

What LitElement Provides

LitElement extends HTMLElement (via ReactiveElement, an intermediate class) and adds three things: automatic Shadow DOM attachment, a reactive property system that schedules re-renders, and a render() contract that returns an html template to project into that Shadow DOM.

You rarely interact with ReactiveElement directly — LitElement is the class you extend in practice, since it adds the render()/templating layer on top of ReactiveElement's property-and-update-scheduling foundation.

import { LitElement, html } from 'lit';

class StatusBadge extends LitElement {
  static properties = { status: { type: String } };

  render() {
    return html`<span class="badge">${this.status}</span>`;
  }
}
customElements.define('status-badge', StatusBadge);

LitElement handles attaching Shadow DOM and calling render() whenever status changes — you only describe *what* to render.

The LitElement Class Hierarchy

HTMLElement          (native browser class)
  └─ ReactiveElement (adds reactive properties + update scheduling)
       └─ LitElement (adds render() + html templating)
  • HTMLElement is the native browser base class every custom element must ultimately extend.
  • ReactiveElement (from the @lit/reactive-element package) adds the property/attribute reactivity system.
  • LitElement (from lit) adds the render() method and html/css template integration.
  • In practice, you always import { LitElement } from 'lit' rather than importing ReactiveElement directly.

LitElement Quick Reference

The methods and fields you'll use most often on a LitElement subclass.

Member Purpose
render() Returns the template to display
static properties / @property() Declares reactive fields
static styles / css Declares scoped styles
connectedCallback() Runs when the element is added to the DOM
updated(changedProps) Runs after each re-render
requestUpdate() Manually schedules a re-render
updateComplete Promise that resolves after the next render
shadowRoot Reference to the component's Shadow DOM root

The Reactive Update Cycle, Briefly

When a reactive property changes, LitElement doesn't re-render immediately and synchronously. Instead, it schedules a microtask-based update, batching multiple property changes made in the same tick into a single re-render. This is why setting three properties in a row still only triggers one render() call.

The full lifecycle (covered in depth in a later lesson) runs through shouldUpdate(), willUpdate(), render(), and updated(), but for most components you only ever need to implement render() — the rest have sensible defaults.

this.a = 1;
this.b = 2;
this.c = 3;
// Only ONE render() call happens, after the current microtask,
// reflecting all three changes together.

Awaiting an Update with updateComplete

Because updates are asynchronous, code that needs to inspect the DOM right after a property change should await the updateComplete promise, which resolves once the pending render has finished applying to the DOM.

this.open = true;
await this.updateComplete;
const panel = this.shadowRoot.querySelector('.panel');
console.log(panel.hidden); // reflects the post-render state

Reading this.shadowRoot synchronously right after a property change can see stale DOM — always await updateComplete first.

Common Mistakes

  • Reading this.shadowRoot synchronously after changing a property and expecting the DOM to already reflect the change.
  • Calling render() manually — LitElement calls it for you; manual calls bypass the lifecycle and update scheduling.
  • Importing ReactiveElement directly for a component that also needs templates, instead of the more complete LitElement.
  • Assuming every property change causes an immediate, synchronous DOM write, leading to flaky tests that check the DOM too early.

Key Takeaways

  • LitElement extends ReactiveElement, which extends the native HTMLElement.
  • It adds Shadow DOM attachment, a render() contract, and reactive property-driven update scheduling.
  • Updates are asynchronous and batched — multiple property changes in one tick produce a single render.
  • Await this.updateComplete whenever you need to inspect the DOM immediately after a property change.

Pro Tip

Log this.updateComplete.then(() => console.log('rendered')) while learning Lit to build an intuition for exactly when your batched updates actually land in the DOM.