LIT: A Lightweight Library For Building Web Components

Anthony Rimet
Anthony RimetApril 18, 2024
#web#js

Depending on the assignment we work on, we may use React, Vue, or another frontend framework. If we ever developed a cool component with one frontend, we would have to rewrite it for another. That's where web components come in.

LIT, a library developed by Google, allows to create web components quickly and easily. In this article, I'll explain how to create a simple accordion component with LIT, and how to use it in React and Vue.

Framework-Agnostic Components

I had to develop an Accordion component for React. It looked like this:

import React, { useState } from 'react';
function Accordion({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {title}
      </button>
      {isOpen && <div>{children}</div>}
    </div>
  );
}
export default Accordion;

I also had to develop the same component for Vue. I couldn't reuse any of the React code. I had to write a new one looking like this:

<template>
  <div>
    <button @click="isOpen = !isOpen">
      {{ title }}
    </button>
    <div v-if="isOpen">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isOpen: false
    };
  },
  props: ['title']
};
</script>

This example is simple, but imagine you have a more complex component, with more advanced functionalities. You lose a lot of time rewriting the same code for different frameworks.

That's where Web Components (WC) come in.

Web Components In A Nutshell

Web Components are reusable components that can be used on any web page. They are based on 3 W3C specifications:

  • Custom Elements are used to create personalized HTML elements.
  • Shadow DOM is used to create encapsulated DOM trees, enabling us to have this tree hidden from the current JS and CSS on the page. This means our WC is difficult to break by accident.
  • HTML Templates are used to define reusable HTML templates.

How do Web Components compare to React or Vue? The biggest difference is that they use an imperative programming style instead of a declarative one. The second difference is that Web Components require an object-oriented approach, while React and Vue are functional.

An example of a simple accordion component in web components would look like this:

class AccessibleAccordion extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.isOpen = false;
  }

  connectedCallback() {
    this.title = this.getAttribute('title') || '';
    this.render();
    this.addEventListeners();
  }

  addEventListeners() {
    this.shadowRoot.querySelector('button').addEventListener('click', () => {
      this.isOpen = !this.isOpen;
      this.render();
    });
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        /* Add your styles here */
      </style>
      <button>${this.title}</button>
      ${this.isOpen ? `<div><slot></slot></div>` : ''}
    `;
  }
}

customElements.define('accessible-accordion', AccessibleAccordion);

This style of writing can be tedious as the logic grows. That's why libraries have sprung up to help you get to grips with it, including StencilJS (about which Julien wrote an article a few years ago), Polymer, and Lit.

Lit, A Simple And Fast Library For Building Web Components

LIT, a Google project, has been in the work since 2018. It is now at version 3. It aims to be simple, fast, and lightweight.

LIT

Many companies use LIT, for instance Open Web Components, which offers tools for developing web components. Material 3 is based entirely on LIT. So you can use the same coherent design system in your applications.

LIT offers two Starter kits, including one in TypeScript. For this tutorial, we'll be using the TypeScript starter kit (you can also install LIT directly via npm i lit).

Let's take a look at how to create a simple accordion with LIT.

Building An Accordion With LIT

We'll start by creating a static AccessibleAccordion.ts component in the src/ folder.

import { html, css, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('accessible-accordion')
export class AccessibleAccordion extends LitElement {
    render() {
        return html`
            <div class="accordion">
                <div
                    class="accordion-header"
                    role="button"
                    aria-controls="content-${this.id}"
                >
                    <span>TITLE</span>
                </div>
                 <div id="content-${this.id}" class="accordion-content">
                    <p>CONTENT</p>
                </div>
            </div>
        `;
    }
}

The html function interprets a template literal as an HTML template that can efficiently render to and update a DOM element. LIT allows the same declarative approach as in most frontend framework, without using JSX. In addition, in a LIT component, the HTML properties (like this.id in the above example) are naturally reactive, meaning that when they change, the component will re-render.

Now we can create an index.html file in the public folder to test our component.

<!DOCTYPE html>
<head>
    <script type="module" src="./AccessibleAccordion.js"></script>
</head>
<body>
    <accessible-accordion></accessible-accordion>
</body>

We can now run npm run build && npm run serve and open our browser to see our component in action.

First render

Adding Interactivity

For our accordion, we want to be able to expand and collapse it. We will add a @click event that will update the expanded property.

import { html, css, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('accessible-accordion')
export class AccessibleAccordion extends LitElement {
+   @property() expanded = false;
+   @property() titleAccordion = 'Default title';

+   toggle() {
+       this.expanded = !this.expanded;
+   }

    render() {
        return html`
            <div class="accordion">
                <div
                    class="accordion-header"
                    role="button"
                    tabindex="0"
                    aria-controls="content-${this.id}"
+                   @click="${this.toggle}"
                >
-                   <span>TITLE</span>
+                   <span>${this.titleAccordion}</span>
+                   <span aria-hidden="true">${expanded ? '−' : '+'}</span>
                </div>
+                <div id="content-${this.id}" class="accordion-content" aria-hidden="${!this.expanded}">
                    <p>CONTENT</p>
                </div>
            </div>
        `;
    }
}

I didn't have to mess up with event listeners or the DOM. LIT takes care of that for me. I just have to update the properties, and the component will re-render itself. This is much easier than with vanilla Web Components.

Styling Our Component

We can style our component by setting CSS styles in the static styles property using a css template literal. The purpose is to automatically scope our style to the shadow root.

import { html, css, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('accessible-accordion')
export class AccessibleAccordion extends LitElement {
+   static styles = css`
+      .accordion {
+        border: 1px solid #ccc;
+        border-radius: 4px;
+        margin: 0 0 10px;
+        overflow: hidden;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+      }

+      .accordion-header {
+        background-color: #f7f7f7;
+        color: #444;
+        cursor: pointer;
+        padding: 18px;
+        text-align: left;
+        border-bottom: 1px solid #ddd;
+        font-weight: bold;
+        display: flex;
+        justify-content: space-between;
+      }

+      .accordion-header[aria-expanded='true'] {
+        background-color: #ddd;
+      }

+      .accordion-content {
+        background-color: white;
+        border-top: 1px solid #ccc;
+        box-sizing: border-box;
+        transition: max-height 0.6s ease;
+        padding: 15px;
+      }

+      .accordion-content[aria-hidden='true'] {
+        max-height: 0;
+        overflow: hidden;
+        padding: 0px;
+        transition: max-height 0.6s ease;
+      }
+   `;

    @property() expanded = false;
    @property() titleAccordion = 'Default title';
    // ...
}

Scoped styles are a great feature of LIT. They allow us to write CSS without worrying about the global scope. The styles are encapsulated in the shadow DOM, so they won't affect the rest of the page.

Slots

One of the main objectives of our accordion is to be able to pass content to it. We can use the slot API to do this. Web Components can only accept children if we define a slot for it. The default value for a slot acts as a placeholder.

import { html, css, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('accessible-accordion')
export class AccessibleAccordion extends LitElement {
    // ...
    render() {
        return html`
            <div class="accordion">
                <div
                    class="accordion-header"
                    role="button"
                    tabindex="0"
                    aria-controls="content-${this.id}"
                    @click="${this.toggle}"
                >
                    <span>${this.titleAccordion}</span>
                    <span aria-hidden="true">${expanded ? '−' : '+'}</span>
                </div>
                <div id="content-${this.id}" class="accordion-content" aria-hidden="${!this.expanded}">
-                   <p>CONTENT</p>
+                   <slot></slot>
                </div>
            </div>
        `;
    }
    toggle() {
        this.expanded = !this.expanded;
    }
}

We can now pass a child to our component to define the accordion content:

<!DOCTYPE html>
<head>
    <script type="module" src="./AccessibleAccordion.js"></script>
</head>
<body>
    <accessible-accordion titleAccordion="Hello Marmelab">
        <p>Content accordion</p>
    </accessible-accordion>
</body>

The accordion is now fully interactive and can be styled. You can see the final result below:

Using Web Components In React And Vue

Now that we have our web component, we can use it in a web application through a frontend framework.

Vue offers native support for web components:

<template>
  <div id="app">
    <accessible-accordion titleAccordion="Hello Marmelab from Vue">
      <p>Content accordion</p>
    </accessible-accordion>
  </div>
</template>

<script>
export default {
  name: 'App',
};
</script>

Working with web components in React, on the other hand, can be frustrating. For example, React requires each of its components to start with a capital letter, so you have to adapt the web component or create a new interface.

function App() {
  return (
    <div className="App">
      <accessible-accordion titleAccordion="Hello Marmelab from React">
        <p>Content accordion</p>
      </accessible-accordion>
    </div>
  );
}
export default App;

Note that React 19, the next major version of the framework, will greatly improve the support for Web Components.

Conclusion

In broad terms, LIT doesn't differ much from StencilJS. However, I find it lighter and faster. The runtime library is only 5kB. It has tools that I didn't mention during my discovery, such as internationalization, tests, server rendering, etc.

I think it's ideal for creating an application with logic, and if you want to go through libraries like Vue and React.

This library was fun to discover, and I'd like to see how it's used in cross-framework projects. You can retrieve the accordion in this gist. If you have any feedback, please don't hesitate to share it with us.

Did you like this article? Share it!