Lit is an interesting option among front-end JavaScript frameworks for reactive programming. It’s caught quite a bit of interest from developers, but remains relatively under-the-radar compared to some other reactive frameworks. Lit is built on top of the Mozilla Web Components standard and prioritizes speed and a small set of useful features.
The Mozilla Web Components standard
To understand Lit, you have to understand Web Components. A browser standard supported by all the major browsers, Web Components provides a consistent way to define UI components. The idea of Web Components is to give developers a set of tools in the browser to handle the universal needs of UI components. In an ideal world, every framework—be it React, Vue, or something else—would sit atop the Web Components layer, lending more consistency to web development.
Lit is a clean, focused library that facilitates a more comfortable developer experience of using Web Components. It works by producing web components, which are just custom HTML elements. These elements can be used broadly, for example, in React. Here’s a simple greeting component built from the standard:
class SimpleGreeting extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const name = this.getAttribute('name') || 'World';
this.shadowRoot.innerHTML = `
p {
color: navy;
font-family: sans-serif;
border: 1px solid lightblue;
padding: 5px;
display: inline-block;
}
Hello, ${name}!
`;
}
}
This component outputs a greeting based on the name
property, with simple component-scoped styling. To use it, you can enter it into the web console (F12) and then run:
const defaultGreeting = document.createElement('simple-greeting');
document.body.appendChild(defaultGreeting);
How the component works and what it does is fairly obvious, although there are several interesting features, like the constructor and the shadowRoot
. Mainly, the thing to notice is that Web Components lets you define encapsulated functionality using a browser standard, which can be run directly in the web console.
Developing web components with Lit
Now let’s look at the same functionality, but using Lit.
Lit provides helper classes and functions like LitElement
and decorators like customElement
along with html
and css
functions to streamline the development process:
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('simple-greeting-lit')
export class SimpleGreetingLit extends LitElement {
@property({ type: String })
name="World"; // default
static styles = css`
p {
color: blueviolet;
font-family: sans-serif;
border: 2px solid mediumpurple;
padding: 8px;
display: inline-block;
}
span {
font-weight: bold;
}
`;
render() {
return html` Hello, ${this.name} ! This is Lit.
`;
}
}
This code snippet serves the same purpose as our Web Components example, but you can see right away that the size and complexity have been reduced. The decorators (aka annotations) beginning with @
let us declare the customElement
(which is what the Web Component ultimately was doing) and the name
property in a concise way. We have also dropped the default constructor and no longer require inline markup for the CSS, thanks to Lit’s css
function (a tagged template literal function).
Lit also lets us use the render
method to return a template generated by the html
function. The content of the html
function argument lets you combine HTML with variable interpolation. This is similar to JSX and other templating syntax, but notice that we use ${}
instead of {}
, and that we use this
to refer to the component.
The easiest way to try this out is using the Lit online playground. Note that in this playground, you’ll need to use the TS (TypeScript) toggle for the annotations to work. (This limitation only pertains to the playground; annotations will work with JavaScript in the build.)
Adding reactivity to Lit components
Now let’s take the next step in reactivity and make Lit’s name
variable interactive. We’ll add an input that lets us change the name—a two-way binding between an input
component and the name displayed in the template. Lit keeps them in sync.
The following code includes only the meaningful parts that have changed:
render() {
return html`
Hello, ${this.name} !
`;
}
_handleNameInput(event: Event) {
const inputElement = event.target as HTMLInputElement;
this.name = inputElement.value;
}
The functionality here is the same as the previous sample, but now we have an input
element and a handler function. The input is standard HTML type text. It’s also a standard value
property, but it is prefixed with Lit’s dot operator. The dot operator binds the input to ${this.name}
, the magic ingredient that makes the input’s value reactive for that variable. The dot operator tells Lit that you want the live JavaScript property for the value, and not a static value. This ensures Lit will keep the input up-to-date with any programmatic changes to the value.
The @input
attribute lets us point the change handler at our _handleNameInput
function. The function itself uses standard DOM manipulation to retrieve the value of the input element and then assign that to the the.name
variable. That is the other side of the two-way binding. When the user changes the value inside the input, the handler updates this.name
. Lit ensures that wherever this.name
appears, it gets the new value.
Using internal component state in Lit
Another essential feature common to all reactive libraries is the internal component state. Lit also simplifies this aspect of reactive programming. For example, let’s say we need a show/hide feature. This would depend on a purely internal boolean value, so there is no need to connect it with a property that interacts with a parent or anything external. We can declare a new state variable like so:
@state()
private _showSecretMessage = false;
Now this will be available to us in the UI. We can use it to toggle the visibility of a section:
${this._showSecretMessage
? html` This is the secret message!
`
: '' /* Render nothing if false */
}
This will go in the template, as part of the render
function. It uses a template expression (the ${}
construct) and within that, a JavaScript ternary operator (the ? :
syntax). This will evaluate to the segment following the ?
if this._showSecretMessage
is true, or the part following :
if it’s false. The net result is, if the value is true, we get a chunk of template HTML placed into the view at this point, and if not, we get nothing.
And that’s exactly what we want—conditional rendering based on our toggle. To actually toggle the value, we can add a button:
${this._showSecretMessage
? html` This is the secret message!
`
: '' /* Render nothing if false */
}
This button code uses the state variable to conditionally show an appropriate label. Here’s how the @click
handler looks:
_toggleSecretMessage() {
this._showSecretMessage = !this._showSecretMessage;
}
Here, we simply swap the value of our state variable, and Lit does the work of manifesting that change in the view based on our ternary display. Now, we have a panel we can show and hide at will.
Rendering collections in Lit
Now let’s check out Lit’s ability to render collections. First, we’ll create a list of Hobbits
as a property:
@property({ type: Array })
hobbits = ["Frodo Baggins", "Samwise Gamgee", "Merry Brandybuck", "Pippin Took"];
We’re using a property here instead of state because we’ll likely set this value from a parent. Next, we want to display our Hobbits
:
The Fellowship's Hobbits:
${this.hobbits && this.hobbits.length > 0
? html`
${this.hobbits.map(
(hobbitName) => html` - ${hobbitName}
`
)}
`
: html` (No hobbits listed in this roster!)
`
}
We use the ternary conditional operator again to show a message if the Hobbits
are empty. With our default data, we show a list of the most famous Hobbits (all except Bilbo). The main work is done by using the map
functional operator on the this.hobbits
variable. This lets us move over each element and output the appropriate list-item markup via Lit’s html
function.
Using Lit to make API calls
Now let’s switch from Middle Earth to Westeros and load some character data from a remote API.
First, we’ll create an internal state
variable to manage the fetch promise:
@state()
private _characterDataPromise: Promise ;
Next, we’ll implement a constructor
because we need to do something when first loading the component. In this case, we’re loading the data:
constructor() {
super();
this._characterDataPromise = this._fetchCharacterData();
}
Here, we call out to the _fetchCharacterData
function:
private async _fetchCharacterData() {
const apiUrl = "https://www.anapioficeandfire.com/api/characters?page=1&pageSize=10";
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`API request failed with status: ${response.status}`);
}
const json: Array = await response.json();
if (json && json.length > 0) {
const characterTemplates = json.map((char) => {
const displayName = char.name || (char.aliases && char.aliases[0]) || "Unnamed Character";
return html`
${displayName}
${char.culture ? html` - Culture: ${char.culture} ` : ''}
${char.born ? html` , Born: ${char.born} ` : ''}
`;
});
return html` ${characterTemplates}
`;
} else {
return html` No characters found in these lands!
`;
}
} catch (error) {
console.error("Failed to fetch Game of Thrones character data:", error);
return Promise.resolve(html` `);
}
}
The code here is primarily standard JavaScript, except that we’re using Lit’s html
function to return appropriate template markup for each case in our fetch results. But notice that the actual _fetchCharacterData
function returns a promise. In the case of an error, it does so explicitly, but in all cases, the async function will return a promise. Note, also, that the resolve
method is called with the contents of the html
function call.
We saved a handle to this promise earlier in this._characterDataPromise
. The saved handle lets us wait intelligently on the outcome of this call, in the main component template:
return html`
Characters from the Seven Kingdoms (or thereabouts):
${until(
this._characterDataPromise,
html` `
)}
`;
Again, we use the until()
function to await the promise’s final outcome. Note that the second argument displays the waiting content.
Conclusion
Lit contains a wealth of interesting ideas, and its popularity is unsurprising, especially given its foundation in the Web Components standard. The big question is whether Lit will take off as a universal component system for a range of other frameworks such as React, Svelte, and Vue. If it does, we’ll enter a whole new phase in its relevance and adoption. For now, though, Lit is a viable approach on its own, especially attractive for projects that put a high value on standards compliance.
See my GitHub repository for the source code for all examples in this article.