DEV Community

Rob Levin
Rob Levin

Posted on

Let's Play with Lit

1. Setting Up Lit

Probably the most straight forward way to start using Lit is to set it up using vite:

npm create vite@latest my-lit-app -- --template lit-ts
Enter fullscreen mode Exit fullscreen mode

Then you'll be able to cd into the directory and just follow the direction. Alternatively, you can use a CDN in an HTML file:

<script src="https://cdn.skypack.dev/lit"></script>
Enter fullscreen mode Exit fullscreen mode

Lit is a modern framework for creating web components with a minimal API and optimal performance. It handles efficient rendering and easy reactivity for component data.


2. Creating a Simple Web Component

Lit works by extending the LitElement base class. Here's a basic component:

import { LitElement, html, css } from 'lit';

class MyComponent extends LitElement {
  static styles = css`
    :host {
      display: block;
      color: blue;
    }
  `;

  render() {
    return html`<p>Hello, Lit!</p>`;
  }
}

customElements.define('my-component', MyComponent);
Enter fullscreen mode Exit fullscreen mode
  • LitElement: The base class for component logic, lifecycle hooks, reactivity, and rendering.
  • html: A tagged template literal for writing HTML and updating DOM elements.
  • css: A tagged template literal for defining scoped component styles.

Why LitElement?

LitElement provides reactive data binding and an efficient rendering engine. When a component's properties change, it updates only the dynamic parts of the DOM, which minimizes unnecessary re-renders for better performance. This process is known as DOM Diffing.


3. Using Reactive Properties

In Lit, you declare reactive properties via the static properties getter to trigger re-renders when their values change.

static properties = {
  place: { type: String }
};

constructor() {
  super();
  this.place= 'World';
}

render() {
  return html`<p>Hello, ${this.place}!</p>`;
}
Enter fullscreen mode Exit fullscreen mode
  • Reactivity: Lit's reactivity engine observes changes to properties (like place), and changes and automatically triggers a re-render.

4. Handling Events

Event handling is done in Lit using a familiar @event syntax. For example, to handle clicks:

render() {
  return html`<button @click="${this._handleClick}">Click me</button>`;
}

_handleClick() {
  console.log('The button was clicked...');
}
Enter fullscreen mode Exit fullscreen mode

Here, @click binds the button's click event to the _handleClick method.

5. Styling Components

In Lit, styling is done via the css tagged template literal and scoped to the component. This means styles won’t inadvertantly leak.

static styles = css`
  :host {
    display: block;
    color: blue;
  }
  button {
    background: green;
  }
`;
Enter fullscreen mode Exit fullscreen mode

6. Slots

You can create custom components that allow for content projection using the <slot> element. This lets users pass their content into the component.

render() {
  return html`<slot></slot>`;
}
Enter fullscreen mode Exit fullscreen mode

7. Lifecycle Methods

Lit provides lifecycle methods that give you hooks into the component’s life cycle, such as connectedCallback(), disconnectedCallback(), and updated().

Example:

connectedCallback() {
  super.connectedCallback();
  console.log('Component was connected to the DOM!');
}
Enter fullscreen mode Exit fullscreen mode

8. Using Directives for Dynamic Behavior

Lit provides directives like ifDefined for conditional rendering and repeat for iterating over lists. This makes it easy to handle dynamic content.

Example:

import { ifDefined } from 'lit/directives/if-defined.js';

render() {
  return html`<div>${ifDefined(this.someValue ? this.someValue : undefined)}</div>`;
}
Enter fullscreen mode Exit fullscreen mode

Why directives?

Directives can provide advanced capabilities perhaps similar to hooks in React, but are a topic we'll save for longer article in the future.


How You Might Create a Simplified UI Component for AgnosticUI Framework

I'm rewriting my UI library AgnosticUI in Lit for the core primitive components. I'll write another article on "the whys" but for now, let's take a simplified version of one of my components to explore how you might use Lit in building a component library.

The AgnosticUI Button is a rich, accessible button component with a variety of states and styles, and it uses custom design tokens for theming. Here, I've simplified it so hopefully you can see some of the possibilities of what Lit has to offer for such an endeavor (yes, I like to use words like endeavor—and that em dash was me not AI):

1. Component Setup

The AgButton class would extend LitElement and define properties for all the button’s attributes (variant, size, shape, disabled, etc.). For reactivity, each of these would be defined as a @property.

In Lit, the css tagged template literal is perfect for handling those dynamic styles using design tokens. Again, we'll simplify the button and show how variant and size styles could be written. You'll need to imagine that you already have already setup some CSS custom properties:

export class AgButton extends LitElement {
  static styles = css`
    /* :host refers to the AgButton or ag-button itself */
    :host {
      display: inline-flex;
      justify-content: center;
      align-items: center;
    }
    button {
      background: var(--ag-background-tertiary);
      color: var(--ag-text-color);
    }

    /* <ag-button variant="primary">My Button</ag-button> */
    :host([variant="primary"]) button {
      background: var(--ag-primary);
      color: white;
    }

    /* <ag-button size="md">My Button</ag-button> */
    :host([size="md"]) button {
      padding: var(--ag-space-2);
      font-size: var(--ag-font-size-base);
    }
  `;
Enter fullscreen mode Exit fullscreen mode

3. Conditional Styling

The AgButton has various states like loading, pressed, and disabled. You can handle these states dynamically in Lit using the render() method, where you can bind properties to aria-* attributes for accessibility and manage class or style changes for different states.

render() {
  return html`
    <button 
      ?disabled=${this.disabled || this.loading}
      aria-pressed=${this.toggle ? String(this.pressed) : undefined}
      aria-label=${ifDefined(this.ariaLabel)}
      @click=${this._handleClick}
    >
      <slot></slot>
    </button>
  `;
}
Enter fullscreen mode Exit fullscreen mode

4. Event Handling & Interaction

For handling the click, focus, or blur events, you can use Lit’s event handling syntax (@click, @focus, etc.) and pass custom events like we've shown earlier.

For example, toggling the pressed state could dispatch a custom toggle event:

_handleClick() {
  if (this.toggle) {
    this.pressed = !this.pressed;
    this.dispatchEvent(new CustomEvent('toggle', { detail: { pressed: this.pressed }, bubbles: true, composed: true }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Using Lit to build web components provides a rich set of reactive properties, scoped styles, and declarative event handling, and minimizes boilerplate. This, in turn, improves maintainability and dUX while also keeping your component behaviors predictable and consistent. As I mentioned, I'm currently rebuilding AgnosticUI atop of Lit, so you're welcome have a look as I "build in public" in the upcoming weeks. Here's the feature branch for v2.

Top comments (0)