Frontend Development 23 min read

Practical Guide to Web Components with LitElement

This article provides a comprehensive overview of Web Components and demonstrates how LitElement simplifies component creation, reactive properties, templating, styling, lifecycle management, complex data handling, two‑way binding, and directives, offering code examples and best‑practice recommendations for modern frontend development.

政采云技术
政采云技术
政采云技术
Practical Guide to Web Components with LitElement

Preface

Google introduced the concept of Web Components in 2011, relying on Custom Elements, Shadow DOM, and HTML Templates. After years of limited browser support, the technology became production‑ready in 2015, but compatibility issues persisted.

During that time, frameworks like Angular, React, and Vue rose to prominence, each offering component models tightly coupled to the framework, which kept demand for framework‑agnostic Web Components high.

With broad browser support and polyfills, compatibility is no longer a barrier, and developers are actively exploring Web Components.

LitElement offers a lightweight framework to simplify native Web Component development.

Review

Previous article covered native Web Components basics: three core elements and lifecycle, attribute/property communication, component library design, and usage in native, React, and Vue contexts.

Native Web Component code can be verbose, especially for attribute observation, complex data passing, and two‑way binding.

LitElement addresses these pain points by providing reactive properties and a declarative templating system, built on the lit‑element and lit‑html libraries.

LitElement Introduction

Basic Concepts

Lit provides a base class that offers reactivity, scoped styles, and a concise declarative template system with TypeScript support, requiring no build step.

LitElement extends HTMLElement via ReactiveElement, inheriting all standard element properties and methods.

export class LitButton extends LitElement { /* ... */ }
customElements.define('lit-button', LitButton);

Components are registered as custom elements and can be used like any native HTML tag.

<lit-button type="primary"></lit-button>

Rendering

The render() method returns a TemplateResult created by the html tag function, updating only changed expressions.

export class LitButton extends LitElement {
  render() {
    return html`
`;
  }
}
customElements.define('lit-button', LitButton);
TemplateResult is the pure value object produced by lit‑html after processing a template string.

Render can return primitives, TemplateResult objects, DOM nodes, or iterable collections.

Reactive Properties

Attributes are string‑only HTML tag properties; properties are JavaScript object fields. Lit maps attributes to properties and can reflect changes back to attributes.

Define reactive properties via a static properties field, configuring options such as type , reflect , converter , hasChanged , and state .

export class LitButton extends LitElement {
  static properties = {
    type: { type: String, reflect: true },
    other: { type: Object }
  };
}

Options control attribute association, type conversion, change detection, reflection, and internal state handling.

Styling

Component templates render into a Shadow DOM; static styles defined with the css tag apply only to the component’s shadow root, ensuring encapsulation.

export class LitButton extends LitElement {
  static styles = css`
    .lit-button {
      display: inline-block;
      padding: 4px 20px;
      font-size: 14px;
      line-height: 1.5715;
      font-weight: 400;
      border: 1px solid #1890ff;
      border-radius: 2px;
      background-color: #1890ff;
      color: #fff;
      box-shadow: 0 2px #00000004;
      cursor: pointer;
    }
  `;
}

Lit also provides classMap and styleMap directives for conditional class and style application.

import { LitElement, html, css } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';

export class LitButton extends LitElement {
  static properties = { classes: {}, styles: {} };
  static styles = css`...`;
  constructor() {
    super();
    this.classes = { 'lit-button': true, someclass: true, anotherclass: true };
    this.styles = { fontFamily: 'Roboto' };
  }
  render() {
    return html`
`;
  }
}
customElements.define('lit-button', LitButton);

Lifecycle

LitElement inherits standard Custom Element callbacks (constructor, connectedCallback, disconnectedCallback, attributeChangedCallback, adoptedCallback) and adds a reactive update cycle (hasChanged, requestUpdate, shouldUpdate, willUpdate, update, render, firstUpdated, updated, updateComplete, performUpdate, hasUpdated, getUpdateComplete).

connectedCallback() {
  super.connectedCallback();
  addEventListener('keydown', this._handleKeydown);
}

disconnectedCallback() {
  super.disconnectedCallback();
  window.removeEventListener('keydown', this._handleKeydown);
}

Passing Complex Data Types

Use the dot ( . ) prefix in templates to bind non‑string values directly, allowing objects, arrays, and dates to be passed without manual serialization.

/** Parent component – complex data */
import { html, LitElement } from 'lit';
import './person';

class LitComplex extends LitElement {
  constructor() {
    super();
    this.person = { name: 'cai' };
    this.friends = [{ name: 'zheng' }, { name: 'yun' }];
  }
  render() {
    return html`
Complex data type
`;
  }
}
customElements.define('lit-complex', LitComplex);
export default LitComplex;
/** Base component */
import { html, LitElement } from 'lit';

class LitPerson extends LitElement {
  static properties = {
    person: { type: Object },
    friends: { type: Array },
    date: { type: Date }
  };
  firstUpdated() {
    console.log(this.person instanceof Object, this.friends instanceof Array, this.date instanceof Date);
  }
  render() {
    return html`
${this.person.name} has ${this.friends.length} friends
`;
  }
}
customElements.define('lit-person', LitPerson);
export default LitPerson;

Two‑Way Data Binding

/** Father component */
import { html, LitElement } from 'lit';
import './lit-input';

class LitInputFather extends LitElement {
  static properties = { data: { type: String } };
  constructor() {
    super();
    this.data = 'default';
  }
  render() {
    return html`
`;
  }
}
customElements.define('lit-input-father', LitInputFather);
export default LitInputFather;
/** Input component */
import { html, LitElement } from 'lit';

class LitInput extends LitElement {
  static properties = { value: { type: String, reflect: true } };
  change = (e) => { this.value = e.target.value; };
  render() {
    return html`
Input:
`;
  }
}
customElements.define('lit-input', LitInput);
export default LitInput;

The child component reflects its value property back to the attribute, enabling the parent to read the updated value, achieving a form of two‑way binding.

Directive Usage

Lit provides built‑in directives such as cache to preserve DOM between renders, improving performance for large template switches.

import { LitElement, html } from 'lit';
import { cache } from 'lit/directives/cache.js';

class LitCache extends LitElement {
  static properties = { show: false, data: {} };
  constructor() {
    super();
    this.data = { detail: 'detail', sumary: 'sumary' };
  }
  detailView = (data) => html`
${data.detail}
`;
  summaryView = (data) => html`
${data.sumary}
`;
  changeTab = () => { this.show = !this.show; };
  render() {
    return html`
      ${cache(this.show ? this.detailView(this.data) : this.summaryView(this.data))}
切换
`;
  }
}
customElements.define('lit-cache', LitCache);

Lit also offers many other directives, mixins, and decorators for advanced use cases.

Conclusion

LitElement brings several advantages to Web Component development:

Simplicity – builds on the Web Components standard with added reactivity and declarative templates.

Speed – updates only the parts of the UI that change.

Lightweight – compressed size around 5 KB.

Extensibility – lit‑html’s template literals enable powerful extensions.

Broad compatibility – works well across modern browsers.

These features satisfy most project scenarios. For deeper learning, refer to the official Lit documentation and the example repository (https://github.com/CYLpursuit/lit-element-ui). The VS Code lit‑plugin extension is recommended for a better development experience.

Final Thoughts

While Web Components may not replace full‑featured frameworks like React or Vue, they coexist and complement each other, handling component encapsulation while frameworks provide routing, state management, virtual DOM, and extensive ecosystems.

UI Component Libraries

shoelace (https://shoelace.style/)

Wired Elements (https://wiredjs.com/)

UI5 Web Components (https://sap.github.io/ui5-webcomponents/playground/getting-started)

Kor (https://kor-ui.com/introduction/welcome)

References

WebComponents | MDN (https://developer.mozilla.org/en-US/docs/Web/Web_Components)

webcomponents/polyfills | GitHub (https://github.com/webcomponents/polyfills)

LitElement | Official Docs (https://lit.dev/docs/components/overview/)

FrontendJavaScriptWeb ComponentsCustom ElementsLitElementReactive Properties
政采云技术
Written by

政采云技术

ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.

0 followers
Reader feedback

How this landed with the community

login 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.