Edit in GitHubLog an issue

Developing add-ons with Lit and TypeScript

Introduction

lit-logo
Lit is a simple library for building fast, lightweight web components. It's built on top of the Web Components standard and provides a set of tools and utilities to simplify the creation of custom elements. Lit uses modern web standards like JavaScript template literals and reactive properties to create reusable and efficient components with minimal boilerplate required.

typescript-logo

TypeScript is a statically typed superset of JavaScript that adds optional static types to the language.TypeScript aims to improve the development experience by providing a robust type system, which helps catch errors early during development and enhances code quality and maintainability.

When you develop add-ons with a combination of Lit and TypeScript, you get the benefits of both worlds; a lightweight component library with reactive properties and templating capabilities, which help you build fast and efficient components, and the robust type system provided by TypeScript.

Lit Key Features

LitElement Base Class

Lit provides the LitElement base class for creating custom elements. It extends the standard HTMLElement and adds reactive properties and templating capabilities. The LitElement class is important to understand when working with Lit, as it provides the foundation for building custom elements.

Template Literals

A template literal is a string literal that allows embedded expressions. It is enclosed in backticks (`) and can contain placeholders (${expression}) for dynamic values. Template literals provide a more flexible and readable way to define strings compared to traditional string concatenation.

Decorators

A decorator is a certain type of declaration that can be attached to a class declaration. It is prefixed with an @ symbol and can be used to modify the behavior of a class or its members. Some popular decorators in Lit include:

  • @customElement: defines a custom element with a given tag name.
  • @property: defines a reactive property that triggers a re-render when its value changes.
  • @state: defines a local state property that triggers a re-render when its value changes. The difference between @state and @property is that @state properties are not exposed as custom element properties. It's useful for managing component-specific state that does not need to be shared with other components.
  • @query: allows you to query for elements in the component's shadow DOM, for instance, to access a button element with the id myButton, you can use @query('#myButton') myButton: HTMLButtonElement;.
  • @eventOptions: allows you to specify event options like capture, once, and passive for event listeners. For example, @eventOptions({ capture: true }) handleClick() { ... }.

Directives

A Lit directive is a special kind of decorator that allows you to extend the template syntax with custom behavior. Some popular directives include:

  • until: waits for a promise to resolve before rendering the content.
  • repeat: repeats a template for each item in an array.
  • ifDefined: conditionally renders content based on the value being defined.
  • guard: prevents re-rendering if the value has not changed.
  • cache: caches the result of an expression to improve performance.
  • live: updates the DOM when a reactive property changes. It's useful when you need to trigger a side effect or update the DOM based on a property change that's not directly related to rendering the component.
  • asyncReplace: asynchronously replaces the content of a template. This directive is useful when you need to fetch data asynchronously and update the template once the data is available.
  • asyncAppend: asynchronously appends content to a template. This directive is useful when you need to fetch data asynchronously and append it to the template once the data is available.
  • css: a directive that creates a CSS template from a template literal. This directive is used to define CSS styles for a component.

render Method

The render method is defined as a template literal that returns the component's HTML structure. It uses the html function from the Lit package to create the template. The render method is called whenever the component needs to be re-rendered, for instance, when a reactive property changes. Some methods that are commonly used in the render method include:

Reactive Properties

Lit uses reactive properties to automatically update the DOM when the state of your component changes. You define properties using decorators like @property. When a property changes, Lit automatically triggers a re-render of the component. This reactive behavior simplifies the process of managing state and updating the UI.

TypeScript Key Features

Static Typing

TypeScript allows you to define types for variables, function parameters, and return values, which helps catch type-related errors at compile time.

Copied to your clipboard
let message: string = "Hello, TypeScript!";

Type Inference

TypeScript can automatically infer types based on the assigned values, reducing the need for explicit type annotations.

Copied to your clipboard
let count = 42; // inferred as number

Interfaces

Interfaces define the shape of an object, specifying the properties and their types. They help enforce consistent object structures.

Copied to your clipboard
interface User {
name: string;
age: number;
}

Classes

TypeScript supports object-oriented programming with classes, including features like inheritance, access modifiers, and decorators.

Copied to your clipboard
class Person {
constructor(public name: string, public age: number) {}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}

Modules

TypeScript uses ES6 module syntax to organize code into reusable modules, making it easier to manage large codebases.

Copied to your clipboard
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
// main.ts
import { add } from "./math";
console.log(add(2, 3));

Generics

Generics allow you to create reusable components that work with various types, providing flexibility and type safety.

Copied to your clipboard
function identity<T>(arg: T): T {
return arg;
}

Add-on Project Anatomy

When you use the CLI to create an add-on based on Lit and TypeScript (ie: the swc-typescript or swc-typescript-with-document-sandbox templates), the CLI generates a project structure that includes the necessary files and configurations to get you started quickly. For instance:

File/FolderDescription
src/index.html
The main HTML template that loads your add-on.
src/index.ts
The entry point for your add-on, where you define your Lit components.
src/ui/components
The directory where you define your Lit components.
src/ui/components/App.ts
The main application component that uses the Adobe Add-On UI SDK to interact with the document sandbox runtime.
src/ui/components/App.css.ts
The CSS styles for the main application component.
src/models
The directory where you define TypeScript interfaces for your add-on APIs.
src/models/DocumentSandboxApi.ts
The TypeScript interface for the APIs exposed by the document sandbox runtime.
src/sandbox/code.ts
The implementation of the document sandbox runtime.
src/sandbox/tsconfig.json
The TypeScript configuration file that specifies the compiler options for your project.

A more in-depth description of the files and folders in the project structure is provided below.

index.html

This is the main HTML file that serves as the entry point for the web application. It includes the custom element <add-on-root>, which is defined in index.ts.

Copied to your clipboard
<body>
<add-on-root></add-on-root>
</body>

index.ts

This file defines the root custom element <add-on-root> using Lit. It initializes the Adobe Add-On UI SDK and renders the <add-on-app> component once the SDK is ready.

Copied to your clipboard
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import "./components/App";
import addOnUISdk from "https://express.adobe.com/static/add-on-sdk/sdk.js";
@customElement("add-on-root") // Lit customElement decorator defines a custom element <add-on-root>.
export class Root extends LitElement {
@state()
private _isAddOnUISdkReady = addOnUISdk.ready;
// The render method returns an HTML template that uses the until
// directive to wait for the Add-On UI SDK to be ready. Once the
// SDK is ready, it renders the <add-on-app> component.
render() {
// This block is a template literal that returns an HTML template
// using the Lit html function. denoted by it being enclosed in
// backticks (`). Dynamic values are inserted using placeholders
// like (${expression}).
return html`
${until(
// The until directive is used to wait for a promise
// to resolve before rendering the content.
this._isAddOnUISdkReady.then(async () => {
console.log("addOnUISdk is ready for use.");
return html`<add-on-app .addOnUISdk=${addOnUISdk}></add-on-app>`;
})
)}
`;
}
}

App.ts

Defines the main application component <add-on-app> using Lit. It uses the Adobe Add-On UI SDK to interact with the document sandbox runtime and provides a button to create a rectangle in the document.

Copied to your clipboard
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { DocumentSandboxApi } from "../../models/DocumentSandboxApi";
import { style } from "./App.css";
import {
AddOnSDKAPI,
RuntimeType,
} from "https://express.adobe.com/static/add-on-sdk/sdk.js";
// The following line defines a custom element <add-on-app> using the Lit
// customElement decorator.
@customElement("add-on-app")
export class App extends LitElement {
@property({ type: Object })
addOnUISdk!: AddOnSDKAPI;
@state()
private _sandboxProxy: DocumentSandboxApi;
static get styles() {
return style;
}
async firstUpdated(): Promise<void> {
const { runtime } = this.addOnUISdk.instance;
this._sandboxProxy = await runtime.apiProxy(RuntimeType.documentSandbox);
}
private _handleClick() {
this._sandboxProxy.createRectangle();
}
// The render method returns an HTML template that uses the .container
// class defined in the CSS.
render() {
// This block is a template literal that returns an HTML template
// using the Lit html function. A template literal in Lit is
// enclosed in backticks (`) and can contain placeholders (${expression})
// for dynamic values.
return html` <sp-theme
system="express"
color="light"
scale="medium"
>
<div class="container">
<sp-button
size="m"
@click=${this._handleClick}
>Create Rectangle</sp-button
>
</div>
</sp-theme>`;
}
}

App.css.ts

Defines the CSS styles for the <add-on-app> component using Lit's css tagged template literal.

Copied to your clipboard
import { css } from "lit"; // Import the css function from the lit package
// The following block defines the CSS styles for the .container class
// using the css tagged template literal. The styles are defined within
// backticks (`) and are passed to the css function to create a CSSResult
// object. A CSSResult object is a representation of CSS that can be applied
// to a LitElement component.
export const style = css`
.container {
margin: 24px;
display: flex;
flex-direction: column;
}
`;

DocumentSandboxApi.ts

Defines the TypeScript interface for the APIs that the document sandbox runtime exposes to the UI runtime. Once you define an interface, any object that implements that interface must implement to the contract defined in the interface. The document sandbox runtime implements this interface in the code.ts file.

Copied to your clipboard
export interface DocumentSandboxApi {
//
createRectangle(): void;
}

code.ts

Contains the implementation of the document sandbox runtime. It defines the createRectangle function and exposes it to the UI runtime (ie: the code running in the iframe in the ui folder).

Copied to your clipboard
import addOnSandboxSdk from "add-on-sdk-document-sandbox";
import { editor } from "express-document-sdk";
// Import the DocumentSandboxApi interface from the models folder
import { DocumentSandboxApi } from "../models/DocumentSandboxApi";
const { runtime } = addOnSandboxSdk.instance;
function start(): void {
// The following block defines a sandboxApi object that implements the
// DocumentSandboxApi interface. Since it implements the interface, it
// must provide an implementation for the createRectangle function.
const sandboxApi: DocumentSandboxApi = {
createRectangle: () => {
const rectangle = editor.createRectangle();
rectangle.width = 240;
rectangle.height = 180;
rectangle.translation = { x: 10, y: 10 };
const color = { red: 0.32, green: 0.34, blue: 0.89, alpha: 1 };
const rectangleFill = editor.makeColorFill(color);
rectangle.fill = rectangleFill;
const insertionParent = editor.context.insertionParent;
insertionParent.children.append(rectangle);
},
};
const sandboxApi: DocumentSandboxApi = {
createRectangle: () => {
const rectangle = editor.createRectangle();
rectangle.width = 240;
rectangle.height = 180;
rectangle.translation = { x: 10, y: 10 };
const color = { red: 0.32, green: 0.34, blue: 0.89, alpha: 1 };
const rectangleFill = editor.makeColorFill(color);
rectangle.fill = rectangleFill;
const insertionParent = editor.context.insertionParent;
insertionParent.children.append(rectangle);
},
};
runtime.exposeApi(sandboxApi);
}
start();

tsconfig.json

Specifies the TypeScript compiler options for your project. It includes settings like the target ECMAScript version, module format, and output directory.

Copied to your clipboard
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"strict": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}

Create a New Lit Component

To create a new component using Lit and TypeScript, follow these steps:

Step 1: Create a new TypeScript file in the src/ui/components directory.

Copied to your clipboard
touch src/ui/components/MyCustomButton.ts

Step 2: Define a new class that extends LitElement and implements your component logic.

Copied to your clipboard
import { LitElement, html } from "lit";
// Import the customElement and state decorators from the lit package
import { customElement, state } from "lit/decorators.js";
@customElement("my-custom-button") // Decorator defines my-custom-button
// Define a custom LitElement component MyCustomButton that extends LitElement.
// The code includes a state property message that holds the text to be
// displayed and a render method that returns an HTML template. The template
// includes a button element that triggers the handleClick method when clicked
// and displays the message property value.
export class MyCustomButton extends LitElement {
@state()
private message = "Hello, Lit!";
render() {
return html`
<sp-button @click="${this.handleClick}">Send</sp-button>
<p>${this.message}</p>
`;
}
handleClick() {
this.message = "Custom Button Clicked!";
}
}

Step 2: Import the Component

To use the new component in your application, import it in the App.ts file and include it in the render method.

Copied to your clipboard
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
// Import the MyCustomButton component
import { MyCustomButton } from "./MyCustomButton";
@customElement("add-on-app")
// Now you can use the MyCustomButton component in the render method of
// the App component. For instance in the block below:
export class App extends LitElement {
...
render() {
return html` <sp-theme system="express" color="light" scale="medium">
<div class="container">
<sp-button size="m" @click=${this._handleClick}>Create Rectangle</sp-button>
<my-custom-button></my-custom-button>
</div>
</sp-theme>`;
}
...
}

FAQ

Q: What are the main benefits of using Lit with TypeScript for Adobe Express add-ons?

A: The combination provides:

  • Lightweight components: Lit creates fast, efficient web components with minimal overhead
  • Type safety: TypeScript catches errors at compile time and improves code maintainability
  • Modern standards: Built on web component standards for future-proof development
  • Reactive properties: Automatic UI updates when component state changes
  • Great developer experience: Excellent tooling, IntelliSense, and debugging support

Q: Do I need to know web components to use Lit?

A: Not necessarily! Lit abstracts away much of the complexity of web components. You work with familiar concepts like classes, properties, and templates. However, understanding the basics of web components (custom elements, shadow DOM) can be helpful for advanced use cases.

Q: What's the difference between @property and @state decorators?

A:

  • @property: Creates a reactive property that's exposed as a custom element attribute. Use for data that comes from parent components or HTML attributes.
  • @state: Creates internal reactive state that's not exposed externally. Use for component-specific state that doesn't need to be shared with other components.

Q: How do I handle events in Lit components?

A: Use the @ syntax in templates to bind event listeners:

Copied to your clipboard
render() {
return html`<button @click=${this.handleClick}>Click me</button>`;
}
handleClick(event: Event) {
// Handle the click event
}

Q: Can I use Lit components with other frameworks like React?

A: Yes! Lit components are standard web components, so they work with any framework or vanilla JavaScript. However, some frameworks (like React) may need special handling for events and properties.

Q: How do I style Lit components?

A: Use the css tagged template literal and the styles static property:

Copied to your clipboard
import { css } from 'lit';
static styles = css`
:host {
display: block;
padding: 16px;
}
button {
background: blue;
color: white;
}
`;

Q: What's the render() method and when is it called?

A: The render() method defines your component's HTML template using the html tagged template literal. Lit automatically calls it when:

  • The component is first created
  • Any reactive property (marked with @property or @state) changes
  • You manually call this.requestUpdate()

Q: How do I query for elements in my component's shadow DOM?

A: Use the @query decorator:

Copied to your clipboard
@query('#myButton')
myButton!: HTMLButtonElement;
// Now you can access this.myButton in your methods

Q: Can I use async operations in Lit components?

A: Yes! Use the until directive for promises and asyncReplace/asyncAppend for async iterables:

Copied to your clipboard
render() {
return html`${until(this.fetchData(), html`Loading...`)}`;
}

Q: How do I communicate between parent and child Lit components?

A:

  • Parent to child: Pass data via properties
  • Child to parent: Dispatch custom events using this.dispatchEvent(new CustomEvent('my-event', { detail: data }))

Q: What TypeScript configuration works best with Lit?

A: The CLI-generated tsconfig.json is a good starting point. Key settings include:

  • "target": "ES2018" or higher for modern features
  • "experimentalDecorators": true for decorator support
  • "strict": true for better type checking

Next Steps

Next, you can explore more advanced features of Lit and TypeScript to enhance your components. Some areas to explore include:

  • Event Handling: Learn how to handle events in Lit components and communicate between components.
  • Component Composition: Explore how to compose multiple components together to create complex UIs.
  • State Management: Implement state management solutions like Redux or MobX to manage the state of your components.
  • Performance Optimization: Optimize your components for performance by using memoization, lazy loading, and other techniques.
  • Testing: Write unit tests for your components using tools like Jest or Mocha to ensure their correctness and reliability.

Check out this handy cheat sheet on properties and state for further reference throughout your development.

  • Privacy
  • Terms of Use
  • Do not sell or share my personal information
  • AdChoices
Copyright © 2025 Adobe. All rights reserved.