Lit for Polymer users

— Updated
Photo of Arthur Evans
Arthur Evans

Lit is a successor to the Polymer library. If you have a project built with Polymer and want to migrate it to Lit, or if you're familiar with Polymer and want to know how it compares to Lit, this is the right document for you.

This document provides a quick overview of how Lit relates to Polymer, and provides a cookbook showing how common Polymer code translates into Lit.

Polymer was one of the first libraries for building web components. Lit is a successor to Polymer, built by the same team and with many of the same aims. The projects share many goals, but Lit takes advantage of lessons learned during the development of Polymer.

Because Polymer is the predecessor to Lit, there are a lot of similarities between the two. Both libraries make it easy to build components that work like built-in HTML elements, and both feature declarative HTML templates.

Lit differs from Polymer in several ways:

  • Lit's rendering is asynchronous and batched by default. With a few exceptions, all Polymer updates are synchronous.

  • Lit exposes an update lifecycle that provides a powerful mechanism for observing changes to properties and computing derived values from them. Polymer has declarative observers and computed properties, but it can be hard to predict the order in which observers will run.

  • Lit focuses on JavaScript-first authoring, using native JavaScript modules. Polymer originally focused on HTML-first authoring, made possible by the HTML Imports specification, which has since been removed from the web platform.

  • Lit expressions use standard JavaScript. Polymer uses a limited domain-specific language in its bindings. Because Lit uses standard JavaScript, you can also use JavaScript for control flow inside expressions (conditional templates and repeated templates), where Polymer uses specialized helper elements.

  • Lit aims for a simple, understandable mental model with unidirectional data flow. Polymer supports two-way data binding and observers, which can be very nice in simple projects, but tend to make it harder to reason about data flow as the project gets more complex.

If you're planning on migrating from Polymer to Lit, you don't have to do it all at once. Polymer and Lit work together, so you can migrate one component at a time.

There are a few things you'll want to do to update your project before you start working with Lit:

  • Update your project to Polymer 3.

  • Make sure your project tooling supports newer JavaScript features.

  • Remove two-way binding.

Polymer 2.x and earlier use HTML imports, which have since been removed from the web platform. Polymer 3 and Lit are both distributed as JavaScript modules, which means they work together well, and can take advantage of a wide range of modern web tooling.

In most cases, most of the migration process can be automated using the Polymer modulizer tool. For more information, see the Polymer 3.0 upgrade guide.

Polymer used features from the ECMAScript 2015 version of the JavaScript spec. If you started from one of the Polymer starter kits, your toolchain may not support newer JavaScript.

Lit uses features from ECMAScript 2019 (and some Lit example code may include newer language features). You'll need to update your tools if they don't handle these newer JavaScript features. For example:

  • Public instance fields and public static fields. These are widely used in the example code:

  • Async and await, which can be used to simplify promise-based code. For example:

For more information on language requirements for Lit, see Requirements.

Polymer's two-way binding effectively tightly couples a host property with a child property. This tight coupling creates a number of issues, so the team chose not to add two-way binding to Lit.

Removing two-way bindings before migrating to Lit will reduce the complexity of your migration, and let you test your application in Polymer without two-way bindings before starting your migration.

If your application doesn't use two-way binding, you can skip this section.

For two-way bindings, Polymer uses its own protocol, which has three main components:

  • A binding from host to child.

  • An event listener that handles property change events from the child element.

  • A facility for the child to automatically fire change events when properties change (notify: true).

This last item is the most problematic. Components fire change events for any change to a property, and each change event is handled synchronously. Widespread use of two-way binding across an entire application can make it hard to reason about data flow and the order in which components update themselves.

Ideally an event is a discrete signal sent to communicate an explicit change that isn't otherwise easily observable. Sending an event as a side-effect of setting a property—as Polymer does—makes the communication potentially redundant and implicit. This implicit behavior, in particular, can make data flow hard to understand.

To summarize the Custom Element Best Practices guidelines, a component should fire events:

  • When the state of the element changes as a result of user interaction—like clicking a button or editing a text field inside the component.

  • When something internal changes inside the component—like a timer going off or an animation completing.

Ideally, a component should fire semantic events, which describe what changed rather than letting low-level UI events bubble out. For example, a form that lets the user update profile data might fire a profile-updated event when the user clicks the Done button. The profile-updated event is relevant to the parent: the click event isn't.

There are many ways to replace two-way bindings. If you're just looking to migrate an existing Polymer application to Lit, you may just want to replace the two-way bindings with code that serves a similar function:

  • Remove the automatic property change event (notify: true).

  • Replace the automatic property change event with a more intentional change event.

  • Replace the two-way binding annotation with a one-way binding and an event handler. (This step is optional, but makes the transition to Lit simpler.)

For example, if the child is updating a notifying property because of a user interaction, remove the notify: true and fire the event change event manually, based on the user interaction event.

Consider the following Polymer code:

This component uses two-way binding to the input element, and has a notifying property, so a parent element can use two-way binding with the name property. The two-way binding to the input makes sense since the binding is set only when the input event fires. However, by adding an event listener for the input event, you can eliminate the notifying property:

This code explicitly listens for input events and fires name-changed events. This code will work with a parent that expects the Polymer two-way binding protocol, so you can update components one at a time.

This code won't fire a property change event when the parent sets the name property—which is a good thing. And you can migrate this code fairly directly to Lit.

Using an event handler like this, you could also add logic to the child component—such as only firing the name-changed event when the user stops typing for a certain interval.

One-way data flow.

Another alternative is to replace two-way bindings with a unidirectional data flow pattern, using a state container like Redux. Individual components can subscribe for updates to the state, and dispatch actions to update the state. We recommend this for new development, but it may require more work if you already have an application based on two-way bindings.

This section shows how Polymer code handles common tasks, and shows the equivalent Lit code. This can be helpful if you're already familiar with Polymer and want to learn Lit; or if you're migrating an existing project from Polymer to Lit.

This section assumes that you're using Polymer 3.0. If you're migrating a project from Polymer to Lit, you'll want to migrate to Polymer 3.0 first.

For more information specific to migrating an existing project, see Migrating from Polymer to Lit.

JavaScript or TypeScript?

Lit works well with either. Most Lit examples are shown using switchable code sample widget, so you can select either TypeScript or JavaScript syntax.

Both Polymer and Lit are based on web components, so defining a component looks very similar in both libraries. In the simplest case, the only difference is the base class.

Polymer:

Lit:

Lit provides an a set of decorators that can improve your developer experience—like the customElement shown in the TypeScript example in the previous section. Note that the TypeScript samples on this site include decorators, while the JavaScript samples omit them, but you can actually use JavaScript with decorators or use TypeScript without decorators. It's up to you. Using decorators in JavaScript requires a compiler like Babel. (Since you already need a compiler for TypeScript, you just need to configure it to process decorators.) For more information, see Decorators.

Both Polymer 3 and Lit provide an html tag function for defining templates.

Polymer:

Lit:

Note that Lit's html is different from Polymer's html. They have the same basic purpose, but they work differently. Polymer's html is called once during element initialization. Lit's html is usually called during each update. Setup work is performed once per template literal string, so subsequent calls to Lit's html function for incremental updates are very fast.

For more information on Lit templates, see Templates overview.

Polymer components usually include styles directly in the template.

In Lit, you typically provide styles in a static styles field using the css tag function.

Adding a style tag directly in the template, like you would in Polymer, is also supported:

Using a style tag may be slightly less performant than the static styles field, because the styles are evaluated once per instance instead of once per class.

For more information, see Styles.

Where Polymer has data binding, Lit has expressions in its templates. Lit expressions are standard JavaScript expressions that are bound to a particular location in the template. As such, Lit expressions can do almost everything you can do with Polymer data bindings, and many things that you can't easily do in Polymer.

Two-way bindings.

The Lit team made an intentional choice not to implement two-way data bindings. While this feature seems to simplify some common needs, in practice it makes it hard to reason about data flow and the order in which components update themselves. We recommend removing two-way binding before migrating to Lit. For more information, see Removing two-way bindings.

Polymer:

Lit:

As you can see, the main difference is replacing the double brackets around the polymer binding:

[[expression]]

with the expression syntax for a tagged template literal:

${expression}

Also, note that the Lit expression uses this.name instead of just name. Polymer bindings only allow certain things inside the binding annotation, such as a property name or path (such as user.id, or shapes[4].type). Property names or paths are evaluated relative to the current binding scope.

You can use any standard JavaScript expression in Lit, and standard JavaScript scopes apply. For example, you can access local variables created inside the render() method, or access instance variables using this.

Like Polymer, Lit supports setting properties, attributes, and event handlers using expressions. Lit uses slightly different syntax, with prefixes instead of suffixes. The following table summarizes the differences between Polymer and Lit binding syntax:

TypePolymerLit
Propertyproperty-name=[[value]].propertyName=${value}
Attributeattribute-name$=[[value]]attribute-name=${value}
Boolean attributeattribute-name?=[[value]]?attribute-name=${value}
Eventon-event-name$=[[handler]]@event-name=${handler}

Notes:

  • Property expressions. Lit uses the literal property name, prefixed with a period. Polymer uses the corresponding attribute name.

  • Event handlers. In Lit, the handler can be either a method, like ${this.clickHandler} or an arrow function. Using an arrow function, you can close over other data or call a function with a different signature. For example:

For more information, see Expressions.

In general, we recommend removing two-way bindings before migrating Polymer projects to Lit. For more information, see Removing two-way binding.

If you've replaced your two-way bindings with one-way bindings and event listeners, you should be able to migrate them fairly directly to Lit.

If you're writing a Lit component to replace a parent component that used to use two-way binding to communicate with a Polymer child component, add a property expression to set the property, and an event listener to handle the property-changed event.

Polymer:

Lit:

Polymer supports conditionals using the dom-if helper element.

In Lit, you can use a JavaScript conditional expressions. The conditional operator (or ternary) works well:

Unlike the Polymer dom-if, the conditional operator lets you supply content for both true and false conditions, although you can also return the empty string to render nothing, as in the example.

For more information, see Conditionals.

By default, Polymer's dom-if behaves a little differently from a Lit conditional. When the condition goes from a truthy to a falsy value, the dom-if simply hides the conditional DOM, instead of removing it from the DOM tree. This may save some resources when the condition becomes truthy again.

When migrating a Polymer dom-if to Lit, you have several choices:

  • Use a simple JavaScript conditional. Lit removes and discards the conditional DOM when a condition changes to falsy. dom-if does the same thing if you set the restamp property to true.

  • Use the standard hidden attribute to hide the content without removing it from the page.

    This is quite lightweight. However, the DOM is created on first render even if the condition is false.

  • Wrap a conditional in the cache directive to avoid discarding and re-creating the DOM when the condition changes.

In most cases, the simple conditional works well. If the conditional DOM is large and complex and you observe delays recreating the DOM when the condition switches to true, you can use Lit's cache directive to preserve the conditional DOM. When using cache, the DOM is still removed from the tree, but is cached in memory, which can save resources when the condition changes.

Since this won't render anything when the condition is falsy, you can use it to avoid creating a complex piece of DOM on initial page load.

Polymer uses the dom-repeat helper for repeating templates.

The template inside the dom-repeat can access a limited set of properties: properties on the host element, plus the item and index properties added by the dom-repeat.

As with conditionals, Lit can handle repeats using JavaScript, by having an expression return an array of values. Lit's map directive works like the map() array method, except that it accepts other kinds of iterables, like sets or generators.

While Polymer bindings can only access certain properties, Lit expressions can access anything available in the JavaScript scope.

You can also generate an array in the render() method:

For more information, see Lists, or try our interactive tutorial on working with lists.

Handling events from repeating templates

There are different ways to add event listeners to repeated elements:

  • If the event bubbles, you can use event delegation, adding a single listener to a parent element.

  • If the event doesn't bubble, you can add a listener to each repeated element.

For examples using both techniques, see Listening to events fired from repeated elements in the Events section.

When handling events from elements generated by repeating templates, you frequently need to identify both the element that fired the event, and the data that generated that element.

Polymer helps with the latter by adding data to the event object.

Lit doesn't add this extra data, but you can attach a unique value to each repeated element for ease of reference:

When adding listeners to individual items, you can also use an arrow function to pass data directly to the event handler:

Lit's reactive properties are a fairly good match for Polymer's declared properties. Reactive properties support many of the same features as declared properties, including syncing values between the property and an attribute.

Like Polymer, Lit lets you configure properties using a static properties field.

Lit also supports using the @property decorator to declare a reactive property. For more information, see About decorators.

Polymer:

Lit:

Both Polymer and Lit support a number of options when declaring a property. The following list shows the Polymer options and their Lit equivalents.

type

Lit's type option serves the same purpose.

value

Lit doesn't support setting a property's value like this. Instead, set a default value in the constructor. If using the @property decorator, you can also use a class field initializer.

reflectToAttribute

In Lit, this option has been shortened to reflect.

readOnly

Lit doesn't include any built-in support for read-only reactive properties. If you need to make a calculated property part of a component's public API, you can add a getter with no setter.

notify

This feature is used to support two-way binding. It was not implemented in Lit because of the issues described in Issues with two-way binding.

Lit components can use the native web APIs (such as dispatchEvent) to fire events in response to user input or when an internal state changes.

computed

Declarative computed properties are not supported in Lit. See computed properties for alternatives.

observer

Lit doesn't support observers directly. Instead, it supplies a number of override points in the lifecycle where you can take action based on any properties that have changed.'

Note that the Polymer example uses a getter—static get properties()—where the Lit JavaScript example uses a class field—static properties. These two forms work the same way. When Polymer 3 was published, support for class fields was not widespread, so Polymer example code uses the getter.

If you're adding Lit components to an existing Polymer application, your toolchain may not support the class field format. In which case, you can use the static getter, instead.

Like Polymer, Lit does dirty checking when properties change to avoid performing unnecessary work. This can lead to issues if you have a property that holds an object or array. If you mutate the object or array, Lit won't detect a change.

In most cases, the best way to avoid these issues is to use immutable data patterns, so that you always assign a new object or array value instead of mutating an existing object or array. The same is generally true for Polymer.

Polymer includes APIs for observably setting subproperties and mutating arrays, but they are somewhat challenging to use properly. If you're using these APIs, you may need to migrate to an immutable data pattern.

For more information, see Mutating object and array properties.

Read-only properties were not a very commonly-used feature in Polymer, and they are not difficult to implement if needed, so they weren't included in the Lit API.

To expose an internal state value as part of the component's public API, you can add a getter with no setter. You may also want to fire an event when the state changes (for example, a component that loads resources from the network may have a "loading" state and fire an event when that state changes).

Polymer's read-only properties include a hidden setter. You can add a private setter for your property if that makes sense for your component.

Note that if the property isn't included in the component's template, you don't need to declare it (with @property or static properties) or call requestUpdate().

Lit provides a number of overridable lifecycle methods that are called when reactive properties change. Use these methods to implement logic that would go in computed properties or observers in Polymer.

The following lists summarize how to migrate different kinds of computed properties and observers.

Computed properties:

  • For values used ephemerally in the template, compute values as local variables in render() and use them in the template before returning.

  • For values that need to be stored persistently, or are expensive to compute, do so in willUpdate().

  • Use the changedProperties.has() method to compute only when dependencies change, to avoid expensive re-computation every update.

Observers:

  • If the observer needs to act directly on DOM based on property changes, use updated(). This is called after the render() callback.

  • Otherwise, use willUpdate().

  • In either case, use changedProperties.has() to know when a property has changed.

Path-based observers:

  • These are complex observers that observe paths like foo.bar or foo.*:

  • This feature was very specific to Polymer's data system that has no equivalent in Lit.

  • We recommend using immutable patterns as a more interoperable way to communicate deep property changes. For more information, see Mutating object and array properties.

If you need to calculate transient values that are only used for rendering, you can calculate them directly in the render() method.

Polymer:

Lit:

Also, because Lit allows you to use any JavaScript expression in a template, Polymer's computed bindings can be replaced with inline expressions, including function calls.

Polymer:

Lit:

Since Lit expressions are plain JavaScript, you need to use this inside the expression to access an instance property or method.

The willUpdate() callback is an ideal place to calculate properties based on other property values. willUpdate() receives a map of changed property values, so you can handle the current changes.

Using willUpdate() lets you choose what to do based on the complete set of changed properties. This avoids issues with multiple observers or computed properties interacting in unpredictable ways.

Mixins were one of several ways to package reusable functionality for use in a Polymer component. If you're porting a Polymer mixin to Lit, you have several options.

  • Standalone functions. Because Polymer's data bindings can only access instance members, people often created a mixin simply to make a function available in a data binding. This isn't required in Lit, since you can use any JavaScript expression in your template. Instead of using a mixin, you can import a function from another module and use it directly in your template.

  • Lit mixins. Lit mixins work much like Polymer mixins, so many Polymer mixins can be reimplemented as Lit mixins. For more information, see Mixins.

  • Reactive controllers. Reactive controllers are an alternate way to package reusable features. For more information comparing mixins to reactive controllers, see Controllers and mixins

Lit components have the same set of standard web components lifecycle callbacks as Polymer components.

In addition, Lit components have a set of callbacks that can be used to customize the reactive update cycle.

If you're using the ready() callback in your Polymer element, you may be able to use Lit's firstUpdated() callback for the same purpose. The firstUpdated() callback is invoked after the first time the component's DOM is rendered. You could use it, for example, if you want to focus an element in the rendered DOM.

For more information, see Completing an update.