Master Lit: A Lightweight Frontend Library for Building Web Components

This article provides a comprehensive overview of Lit, covering its core concepts, key features, decorators, templating, styling, slots, event communication, lifecycle, directives, mixins, controllers, SSR support, and the surrounding ecosystem, offering practical code examples for developers.

ELab Team
ELab Team
ELab Team
Master Lit: A Lightweight Frontend Library for Building Web Components

Understanding Lit

Abstraction and Encapsulation

In the talk “Do you really understand Web Component?” we explored the purpose of frameworks, how they balance rapid development and performance, and how abstraction and encapsulation are common patterns in everyday development, leading to the question of whether web components can be treated the same way.

Introduction to Lit

Lit is a lightweight library for quickly building web components. Its core is the LitElement base class, which provides reactive state, scoped styles, and an efficient template system. Although it builds on native web components, it retains all their features and can be used without any framework.

Key Features

JSX/TSX‑like syntax.

Template syntax similar to template literals.

Supports only native CSS; pre‑processed styles must be compiled by a bundler.

TypeScript support.

Object‑oriented programming model.

One‑way data flow; supports MVVM but no two‑way binding.

Component state managed with @state and @property.

Small responsibility scope, focusing solely on web‑component creation.

Cross‑platform and framework‑agnostic.

Very small bundle size (≈5 KB gzipped).

Custom callbacks can be passed in Lit’s html template via property syntax (undocumented hack).

Lit’s Application and Core Principles

Base Class

The base class LitElement extends the native HTMLElement and adds reactive state, lifecycle hooks, controllers, and mixins.

Decorators

Decorators are syntactic sugar for modifying classes, methods, or properties. @customElement('my-element') registers a custom element (equivalent to window.customElements.define('my-element')). @eventOptions({capture:true, passive:true, once:true}) configures event listeners. @property(options?) test: string = 'Somebody' declares a public property (equivalent to a normal property).

window.customElements.define('my-element');
dom.addEventListener(eventName, func, {capture:true, passive:true, once:true});
class MyElement extends LitElement {
  constructor() {
    super();
    this.test = 'Somebody';
  }
  static get properties() {
    return {
      test: {type: String}
    };
  }
}

State Decorator

class MyElement extends LitElement {
  constructor() {
    super();
    this._active = false;
  }
  static get properties() {
    return {
      _active: {state:true}
    };
  }
}

The options object can contain: attribute: whether the property reflects to an attribute. converter: custom conversion between attribute and property. noAccessor: disables automatic update on property change. reflect: synchronises property changes back to the attribute. type: type declaration. hasChanged: custom change detection function.

HTML Template

Lit’s rendering occurs in the render() method using the html tag function. The template combines HTML with JavaScript expressions, similar to JSX/TSX.

render() {
  if (this.listItems.filter(item => !item.completed).length === 0) {
    return html`<p>anything was done!</p>`;
  }
  return html`
    <ul>
      ${this.listItems.map(item => html`
        <li class=${item.completed ? 'completed' : ''}
            @click=${() => this.toggleCompleted(item)}>
          ${item.text}
        </li>`)}
    </ul>`;
}

Styles

Lit supports native CSS via the css tag. Styles can be defined as a single static string or as an array of css literals. Dynamic styles can be expressed with unsafeCSS.

import {customElement, css, LitElement} from 'lit-element';
@customElement('my-element')
export class MyElement extends LitElement {
  static styles = css`
    p { color: green; }
  `;
}
static styles = [
  css`h1 { color: green; }`,
  css`h2 { color: red; }`
];
import style from './my-elements.less';
static styles = [
  css`:host { width: 500px; }`,
  css`${unsafeCSS(style)}`
];

Slots

Slots work like Vue’s slots because they are defined by the native Web Component spec. An unnamed slot renders all children, while named slots render only matching children.

<my-element test-word="test-word" testWord="testWord">
  <div>I am a child</div>
</my-element>
render() {
  return html`<slot></slot>`;
}

Event Communication

Lit uses the @event syntax to register listeners and dispatchEvent with CustomEvent to emit events.

private addList(e: CustomEvent) {
  this.listItems = [...this.listItems, {text:e.detail, completed:false}];
  this.todoList.requestUpdate();
}
render() {
  return html`<controls-area @addList=${this.addList}></controls-area>`;
}
private sendText() {
  const options = {detail: this.inputText};
  this.dispatchEvent(new CustomEvent('addList', options));
  this.inputText = '';
}

Lifecycle and Update Flow

Lit batches updates: setting multiple properties triggers a single asynchronous update. Only the changed parts of the DOM are re‑rendered. requestUpdate collects changed properties and may call _enqueueUpdate. performUpdate runs shouldUpdate, then willUpdate, update, firstUpdated, and updated. render creates static HTML once; subsequent updates replace only expressions.

Directives

Built‑in directives such as classMap, styleMap, repeat, unsafeHTML, ifDefined, etc., can be used directly in templates. Custom directives extend the Directive class and implement render.

import {Directive, directive} from 'lit/directive.js';
class FormatStr extends Directive {
  render(test: string) {
    return `${test}!!!`;
  }
}
export const formatStr = directive(FormatStr);

Mixins

Mixins share code between classes. The example defines a TestMixin that adds a name property and logs it after the element is connected.

type Constructor<T = {}> = new (...args: any[]) => T;
export const TestMixin = <S extends Constructor<LitElement>>(superClass: S) => {
  class MyMixinClass extends superClass {
    name: string;
    constructor(...args: any[]) {
      super();
      this.name = 'TestMixin';
    }
    connectedCallback() {
      super.connectedCallback();
      setTimeout(() => console.log(this.name), 3000);
    }
  }
  return MyMixinClass as S;
};

Controllers

Controllers are objects that share functionality without rendering. They receive lifecycle callbacks from the host element.

export class MouseController {
  private host: ReactiveControllerHost;
  pos = {x:0, y:0};
  _onMouseMove = ({clientX, clientY}: MouseEvent) => {
    this.pos = {x: clientX, y: clientY};
    this.host.requestUpdate();
  };
  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }
  hostConnected() {
    window.addEventListener('mousemove', this._onMouseMove);
  }
  hostDisconnected() {
    window.removeEventListener('mousemove', this._onMouseMove);
  }
}

SSR

Lit supports server‑side rendering via the @lit-labs/ssr package.

Ecosystem

Lit’s ecosystem includes routing libraries (e.g., lit-element-router), state management solutions ( lit-element-state), VSCode extensions for syntax highlighting and snippets, and many open‑source component libraries such as Spectrum Web Components, Material‑Components, Carbon Web Components, and others.

frontendJavaScriptLibraryweb componentstemplatesLit
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.