Post

Lit Web Components

Lit Web Components

Lit Web Components

Lit

Lit is a lightweight JavaScript library for building reusable Web Components. The best place to test it is at the Lit Playground.

An example structure of a LitElement is very similar to that of a regular custom element in JavaScript, but a fair amount of the heavy-lifting is done by Lit, such as parsing, diffing, and DOM updates. Additionally Lit handles automatically adding a “shadow DOM” to each element, which would have been manually achieved via element.attachShadow({ mode: 'open' }).

Just as with a regular custom element it is essential to add the line customElements.define('custom-element', CustomElement) after defining the class, as this is what is used to ensure the HTML recognises the element. As such it is important that the script is imported before the custom tag is used in the DOM. This issue is slightly different if your site tags are primarily generated via JavaScript as the .define method will fire when the module is imported to the currently running script, but importing the script doesn’t actually do much other than ensure the element is usable, as you don’t need the import in order to use document.createElement('custom-element'), but without it you will run into errors in the HTML. It is worth noting that JavaScript has a “smart” import system and won’t redefine custom elements every time that you import them, so it is best practice to add the import line to any connected components, and to the main script.

A Note on Web Components

As per the mdn web docs: “Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilise them in your web apps”

In essence it is a design pattern for making reusable code for HTML / CSS / JavaScript. There’s no exact science to a Web Component, you could use HTML templates via <template> and <slot>, as well as a “shadow DOM”. It is similar to writing .jsx files, except that here you are using vanilla JavaScript to encapsulate HTML / CSS in string form within JavaScript.

A Note on Shadow DOMs

A “shadow DOM” (sDOM for later reference) is essentially a DOM tree that is separate from the main DOM. This sDOM is internal to the element and styling from CSS does not leak in or out, such that the element is only affected by its internal styling defined within its sDOM.

A sDOM can be “open” or “closed” (attachShadow({ mode: 'open' }) or attachShadow({ mode: 'closed' })). If the sDOM is “open” then it is accessible via JavaScript.

Normally browser dev tools will still show sDom elements.

A Note on ESM (ECMAScript Modules)

The official JavaScript module system, standardised in ES6 in 2015. Allow use of the import /export syntax between different files. This can be done via CDN (Content Delivery Network) with something similar to import { html } from 'https://cdn.jsdelivr.net/npm/lit@3.3.0/+esm', or locally with something similar to import { html } from './node_modules/lit/index.js'

[!NOTE] The syntax for directly importing, as above, is not to be confused with the syntax used in files where build tools are used. In that case you’d see something like import { html } from 'lit', and an associated package.json file

1
2
3
import { html } from './lit.js';    // Relative file path => No build tools
import { html } from 'https://...'; // URL => No build tools
import { html } from 'lit';         // Module name => REQUIRES build tools

Example Lit Components

There are some examples of Lit components below, from the official Lit playground

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Original at https://lit.dev/playground/#sample=examples/hello-world
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/npm/lit@3.3.0/+esm';

export class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'Somebody';
  }

  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}
customElements.define('simple-greeting', SimpleGreeting);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Original at https://lit.dev/playground/#sample=examples/full-component
import {LitElement, html, css} from 'lit';

export class MyElement extends LitElement {
  static properties = {
    greeting: {},
    planet: {},
  };
  // Styles are scoped to this element: they won't conflict with styles
  // on the main page or in other components. Styling API can be exposed
  // via CSS custom properties.
  static styles = css`
    :host {
      display: inline-block;
      padding: 10px;
      background: lightgray;
    }
    .planet {
      color: var(--planet-color, blue);
    }
  `;

  constructor() {
    super();
    // Define reactive properties--updating a reactive property causes
    // the component to update.
    this.greeting = 'Hello';
    this.planet = 'World';
  }

  // The render() method is called any time reactive properties change.
  // Return HTML in a string template literal tagged with the `html`
  // tag function to describe the component's internal DOM.
  // Expressions can set attribute values, property values, event handlers,
  // and child nodes/text.
  render() {
    return html`
      <span @click=${this.togglePlanet}
        >${this.greeting}
        <span class="planet">${this.planet}</span>
      </span>
    `;
  }

  // Event handlers can update the state of @properties on the element
  // instance, causing it to re-render
  togglePlanet() {
    this.planet = this.planet === 'World' ? 'Mars' : 'World';
  }
}
customElements.define('my-element', MyElement);

[!NOTE] Similarly to creating custom elements in vanilla JavaScript, the constructor cannot take arguments

A Note on JavaScript tagFunction

The documentation for the tagFunction can be found here. They are primarily used alongside template literals in JavaScript. A template literal is a string within backticks. The most common use I find is similar to Python’s f-string which has the syntax f"Hello, {name}", with the JavaScript syntax for the same being `Hello, ${name}`.

A tagFunction is a special function that processes a template literal. When you write a “tagged template” like html`Hello, ${name}`, you are using the tagFunction (html) on the template literal. The tagFunction (html) receives the raw string parts and the values of the embedded expressions separately, allowing it to customise how the final output is constructed. In the case of Lit it is enabling features like safe HTML rendering / parsing. It creates a template object that Lit can efficiently update and render to the DOM.

A Note on diffing

The properties attribute of the custom element class above is a list of “reactive” properties to listen for / observe. When one of these properties changes, the render() method is called to update and re-render only the parts of the element that changed. This selective approach to updating elements in the DOM is part of diffing, where two files are compared to see where they differ.

This is a case of single element diffing, Lit sees a reactive property (“reactive prop”) has changed and calls the render() method to get the new html template from html, which is a tagFunction, as in the line return html`...`. With this new html template Lit will “diff” it against the previous template and update only the specific parts of the element that changed, not all elements within it.

This is a fine-tuned / lightweight single element approach to diffing and is contrasted by the approach taken by React and similar, which use a virtual DOM and perform diffing on that full document, only updating the parts of the real DOM where changes are found.

Miscellaneous

  • For testing purposes you can get by with the CDN version import { LitElement, html, css} from 'https://cdn.jsdelivr.net/npm/lit@3.3.0/+esm', otherwise you can install via npm and use a build tool
  • At the time of writing the current version is Lit v3.3.0, you can find the CDN version here
  • Tested on Google Chrome Version 137.0.7151.104 (Official Build) (64-bit)
  • You can find a “starter kit” here but the actual repository layout is quite bloated for a simple learning project
This post is licensed under CC BY 4.0 by the author.