Lit
Background
Lit is a small library for creating web components that is maintained by Google. It aims to improve the experience of authoring web components by eliminating boilerplate and providing more declarative syntax and re-rendering optimizations, and should feel familiar to developers who have experience working with popular component-based front end frameworks.
Mozilla developers began experimenting with using Lit to build a handful of new web components in 2021. The developer experience and productivity benefits were noticeable enough that the team tasked with building out a library of new reusable widgets vendored Lit to make it available in mozilla-central
in late 2022. Lit can now be used for creating new web components anywhere in the codebase.
Using Lit
Lit has comprehensive documentation on their website that should be consulted alongside this document when building new Lit-based custom elements: https://lit.dev/docs/
While Lit was initially introduced to assist with the work of the Reusable Components team it can also be used for creating both reusable and domain-specific UI widgets throughout mozilla-central
. Some examples of custom elements that have been created using Lit so far include moz-toggle, moz-button-group, and the Credential Management team’s login-timeline component.
When to use Lit
Lit may be a particularly good choice if you’re building a highly reactive element that needs to respond efficiently to state changes. Lit’s declarative templates and reactive properties can take care of a lot of the work of figuring out which parts of the UI should update in response to specific changes.
Because Lit components are ultimately just web components, you may also want to use it just because of some of the syntax it provides, like allowing you to write your template code next to your JavaScript code, providing for binding event listeners and properties in your templates, and automatically creating an open shadowRoot
.
When not to use Lit
Lit cannot be used in cases where you want to extend a built-in element. Lit can only be used for creating autonomous custom elements, i.e. elements that extend HTMLElement
.
Writing components with Lit
All of the standard features of the Lit library - with the exception of decorators - are available for use in mozilla-central
, but there are some special considerations and specific files you should be aware of when using Lit for Firefox code.
Using external stylesheets
Using external stylesheets is the preferred way to style your Lit-based components in mozilla-central
, despite the fact that the the Lit documentation explicitly recommends against this approach. The caveats they list are not particularly relevant to our use cases, and we have implemented platform level workarounds to ensure external styles will not cause a flash-of-unstyled-content. Using external stylesheets makes it so that CSS changes can be detected by our automated linting and review tools, and helps provide greater visibility to Mozilla’s desktop-theme-reviewers
group.
The lit.all.mjs
vendor file
A somewhat customized, vendored version of Lit is available at toolkit/content/widgets/vendor/lit.all.mjs. The version of Lit in mozilla-central
has a number of patches applied to disable minification, source maps, and certain warning messages, as well as patches to replace usage of innerHTML
with DOMParser
and to slightly modify the behavior of the styleMap
directive. More specifics on these patches, as well as information on how to update lit.all.mjs
, can be found here.
Because our vendored version of Lit bundles the contents of a few different Lit source files into a single file, imports that would normally come from different files are pulled directly from lit.all.mjs
. For example, imports that look like this when using the Lit npm package:
// Standard npm package.
import { LitElement } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
Would look like this in mozilla-central
:
// All imports come from a single file (relative path also works).
import { LitElement, classMap, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
MozLitElement
and lit-utils.mjs
MozLitElement is an extension of the LitElement
class that has added functionality to make it more tailored to Mozilla developers’ needs. In almost all cases MozLitElement
should be used as the base class for your new Lit-based custom elements in place of LitElement
.
It can be imported from lit-utils.js
and used as follows:
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
class MyCustomElement extends MozLitElement {
...
}
MozLitElement
differs from LitElement
in a few important ways:
It provides automatic Fluent support for the shadow DOM
When working with Fluent in the shadow DOM an element’s shadowRoot
must be connected before Fluent can be used. MozLitElement
handles this by extending LitElement
’s connectedCallback
to call document.l10n.connectRoot
if needed. MozLitElement
also automatically calls document.l10n.translateFragment
on the renderRoot anytime an element updates. The net result of these modifications is that you can use Fluent in your Lit based components just like you would in any other markup in mozilla-central
.
It provides automatic Fluent support for localized Reactive Properties
Fluent requires that attributes be marked as safe if they don’t fall into the default list of allowed attributes. By setting fluent: true
in your Reactive Property’s definition MozLitElement
will automatically populate the data-l10n-attrs
in connectedCallback()
to mark the attribute as safe for Fluent.
class MyCustomElement extends MozLitElement {
static properties = {
label: { type: String, fluent: true },
description: { type: String, fluent: true },
value: { type: String },
};
}
It provides the mapped attribute helpers for standard web attributes
When you want to accept a standard attribute such as accesskey, title or
aria-label at the component level but it should really be set on a child
element then you can set the mapped: true
option in your property
definition and the attribute will be removed from the host when it is set.
Note that the attribute can not be unset once it is set.
class MyElement extends MozLitElement {
static properties = {
accessKey: { type: String, mapped: true },
};
render() {
return html`<button accesskey=${this.accessKey}>Hello</button>`;
}
}
It implements support for Lit’s @query
and @queryAll
decorators
The Lit library includes @query
and @queryAll
decorators that provide an easy way of finding elements within the internal component DOM. These do not work in mozilla-central
as we do not have support for JavaScript decorators. Instead, MozLitElement
provides equivalent DOM querying functionality via defining a static queries
property on the subclass. For example the following Lit code that queries the component’s DOM for certain selectors and assigns the results to different class properties:
import { LitElement, html } from "lit";
import { query } from "lit/decorators/query.js";
class MyCustomElement extends LitElement {
@query("#title");
_title;
@queryAll("p");
_paragraphs;
render() {
return html`
<p id="title">The title</p>
<p>Some other paragraph.</p>
`;
}
}
Is equivalent to this in mozilla-central
:
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
class MyCustomElement extends MozLitElement {
static queries = {
_title: "#title", // equivalent to @query
_paragraphs: { all: "p" }, // equivalent to @queryAll
};
render() {
return html`
<p id="title">The title</p>
<p>Some other paragraph.</p>
`;
}
}
It adds a dispatchOnUpdateComplete
method
The dispatchOnUpdateComplete
method provides an easy way to communicate to test code or other element consumers that a reactive property change has taken effect. It leverages Lit’s updateComplete promise to emit an event after all updates have been applied and the component’s DOM is ready to be queried. It has the potential to be particularly useful when you need to query the DOM in test code, for example:
// my-custom-element.mjs
class MyCustomElement extends MozLitElement {
static properties = {
clicked: { type: Boolean },
};
async handleClick() {
if (!this.clicked) {
this.clicked = true;
}
this.dispatchOnUpdateComplete(new CustomEvent("button-clicked"));
}
render() {
return html`
<p>The button was ${this.clicked ? "clicked" : "not clicked"}</p>
<button @click=${this.handleClick}>Click me!</button>
`;
}
}
// test_my_custom_element.mjs
add_task(async function testButtonClicked() {
let { button, message } = this.convenientHelperToGetElements();
is(message.textContent.trim(), "The button was not clicked");
let clicked = BrowserTestUtils.waitForEvent(button, "button-clicked");
synthesizeMouseAtCenter(button, {});
await clicked;
is(message.textContent.trim(), "The button was clicked");
});