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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
