Framework-Agnostic Design Systems

Using Lit Web Components in Angular
devfest modena logo
Marco Pollacci 04/10/2025 - Modena
Hello Folks!
me Marco Pollacci Senior Frontend Developer
@ 40factory logo
gka
qr

Frameworks come and go

BUT your Design System should stay!

angularjs logo backbone logo ember logo jquery logo qwik logo react logo solid logo svelte logo vue logo angular logo

What exactly is a Design System?

🀨

Design system

  • πŸ“Œ Style guide
  • πŸ“Œ Component library
  • πŸ“Œ Pattern library
design system circle

The Problem

problem meme

Main problems with framework-specific components 🚨

Alright, thanks for the chaos! πŸ˜…

…so, what's our next move? πŸ€”

πŸ₯³ Lit πŸ₯³

BUT FIRST

Web Components!!

rolleye meme

A (not so) new standard πŸ“‹

They're here to stay. 🀘

The building blocks of Web Components πŸ—οΈ

You already use them!

boom meme
Even if you don't know it!
(or at least one of your 3rd party library does)...

Native browser elements <video>

  
    <video controls>
      <source
        src="....."
        type="video/webm"
      />
    </video>

Native browser elements <details>

Native browser elements

Hello from details

  
  <details>
    <summary>Native browser elements</summary>
    <p>Hello from details</p>
  </details>

And much more! α•™( β€’Μ€ α—œ ‒́ )α•—

Ecc..

Finally, Lit πŸ₯³

A modern library for building fast, lightweight, and reusable Web Components.

Lit ✨

"Do more with less" ⚑

Lit embraces a minimalist philosophy: it builds on the web platform, adds only what's necessary, and stays out of your way for everything else.

Our first Lit Web Component

(‒̀ᴗ‒́ )و

Let's Code πŸ‘¨β€πŸ’»
  
  import { html, LitElement } from "lit";
  import { customElement } from "lit/decorators.js";

  @customElement("my-badge")
  export class MyBadge extends LitElement {
    render() {
      return html`<slot></slot> `;
    }
  }

Let's Code πŸ‘¨β€πŸ’»
  //...
  @customElement("my-badge")
  export class MyBadge extends LitElement {
    static styles = css`
      :host {
        display: inline-block; padding: 0.25em 0.4em;
        font-weight: 700; text-align: center;
        border-radius: 0.25rem; color: #fff;
        background-color: #0d6efd;
      }
    `;
    render() {
      return html`<slot></slot> `;
    }
  }
<my-badge>Badge</my-badge> Badge
badge dom
deeper meme

Reactive Properties ⚑

@property()

This decorator allows the component to provide a configurable API, which can be set or updated by the component's users.

@property()

Properties can be fine-tuned with options

@state()

This decorator creates a private reactive field that exists only inside the component, and yet every time its value changes, Lit will still trigger a re-render.

Let's Code πŸ‘¨β€πŸ’»
  //...
  export class MyBadge extends LitElement {
    @property({ type: String }) appearance = "";
    static styles = css`
      :host {
        //other styles with default values and background...
      }
      :host([appearance="secondary"]) {
        background-color: #6c757d;
      }
    `;
    render() {
      return html`<slot></slot> `;
    }
  }
<my-badge appearance="secondary">Badge</my-badge> Badge
badge dom

πŸ₯·πŸΏ Shadow DOM 🀝 Scoped Styles

Let's Code πŸ‘¨β€πŸ’»
  //...
  export class MyBadge extends LitElement {
    @property({ type: String }) appearance = "";
    static styles = css`
        //other styles with default values and background...
        slot {
            color: #fff;
        }
    `;
    render() {
      return html`<slot></slot> `;
    }
  }
<my-badge style="color: #000">Badge</my-badge> Badge
badge dom

Component Lifecycle

lifecycle

Lit components extend HTMLElement and follow its lifecycle. 😎

Lit-Specific Lifecycle Hooks. πŸ”„

Event Handling πŸ“’

Handling events in Lit is straightforward. ✨

Bind events directly in the template using the custom @ syntax.

Let's Code πŸ‘¨β€πŸ’»
  //...
  export class MyButton extends LitElement {

    #handleClick() {
      console.log("Button clicked!");
    }

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

πŸ”” Event handling in Shadow DOM πŸ””

Encapsulation β‰  Isolation: some events can bubble outside the shadow boundary.

Directives πŸ›£οΈ

Directives are special functions in Lit that can customize how template parts are rendered.

They are a powerful tool for conditional rendering, looping, async data fetching, and creating reusable template logic

✧q٩(ΛŠα—œΛ‹ )و✧*q

Why directives?

Some Built-in directives

πŸ” repeat

🧩 when

βš™οΈ map

Let's Code πŸ‘¨β€πŸ’»
  //...
   import { repeat } from 'lit/directives/repeat.js';
  export class MyList extends LitElement {
    items = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
   render() {
        return html`
          <ul>
            ${repeat(
              this.items,
              (item) => item.id //<-- Key function for efficient updates
              (item) => html`<li>${item.name}</li>`
            )}
          </ul>
        `;
      }
  }

πŸš€ Integration πŸš€

Lit without any framework πŸš€

frameworkless meme
Let's Code πŸ‘¨β€πŸ’»

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <script type="module" src="./my-badge.js"></script>
    </head>
    <body>
        <my-badge appearance="secondary">Badge</my-badge>
    </body>
    </html>
  

Lit β™₯️ Angular

Angular + Lit: what's the deal?

Agular has its own component model and template compiler

By default, Angular only recognizes known Angular components

So, when we introduce a Lit Web Component...

...Angular thinks it's just an unknown HTML tag

(α΅•β€”α΄—β€”)
thanks for nothing meme

How to make Angular accept Lit components? 🀨

We need to teach Angular that custom elements are valid Angular provides a special schema: CUSTOM_ELEMENTS_SCHEMA

With this schema, Angular stops complaining πŸ‘Œ

Let's Code πŸ‘¨β€πŸ’»


        import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
        @Component({
          selector: 'app-root',
          template: `<my-badge appearance="secondary">Badge</my-badge>`,
          schemas: [CUSTOM_ELEMENTS_SCHEMA]
        })
        export class AppComponent {}
  

And now...how to import the Library?

From CDN 🌏

Let's Code πŸ‘¨β€πŸ’»

    <html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>My Angular App</title>
        <base href="/" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" type="image/x-icon" href="favicon.ico" />
        <script src="https://cdn.example.com/my-lit-library.js"></script>
    </head>
    <body>
        <app-root></app-root>
    </body>
    </html>
  

From NPM πŸ“¦

Let's Code πŸ‘¨β€πŸ’»


    import '@my-org/my-lit-library/my-lit-library.js';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { appConfig } from './app/app.config';
    import { App } from './app/app';

    bootstrapApplication(App, appConfig).catch((err) => console.error(err));
  

So... integration done? 😎

almost meme

There's still one more thing!

custom-elements.json

What is custom-elements.json?

It's a metadata file that describes your Web Components.

A standard way to document Web Components πŸ“–

It includes details like:

How do we create custom-elements.json?

We can use a library that scans your components and produces the JSON
✧q٩(ΛŠα—œΛ‹ )و✧*q

    //install library
    npm install --save-dev @custom-elements-manifest/analyzer
    
    //add script to package.json
    "analyze": "cem analyze --litelement",
    
    //then, run the script
    npm run analyze
    

πŸŽ‰ Done! πŸŽ‰

And now we finally have custom-elements.json πŸŽ‰

...but wait πŸ‘€

VS Code doesn't magically read it by default πŸ™ƒ ...but IntelliJ actually does (πŸ‘ ΝœΚ–πŸ‘) destroy all meme

custom-elements.json + VS Code β™‘

How do we make it work?
custom-element-vs-code-integration

    //install library
    npm install --save-dev custom-element-vs-code-integration
    
    //create script custom-vscode.js
    import { generateVsCodeCustomElementData } from "custom-element-vs-code-integration";
    import manifest from "./path/to/custom-elements.json";  
    const options = {...};
    generateVsCodeCustomElementData(manifest, options);
    
    //modify script to package.json
    "analyze": "cem analyze --litelement && node custom-vscode.js",
    
    //then, run the script analyze agaiin
    npm run analyze
    
finally done meme

The future of Design Systems

🌐 Native adoption of Web Components keeps growing
🧩 More tooling & integrations will simplify cross-framework use
πŸš€ Companies are already moving to framework-agnostic libraries

The earlier you start, the easier the transition.

thatsall

You can see this slide on

qrcode slide https://talk-framework-agnostic-component-with-lit.pages.dev/devfest-modena-2025
Questions? ✨
    
    @customElement("q-and-a")
    class QandA extends LitElement {
      render() {
        return html`<p>Waiting for your questions... πŸ™‹β€β™€οΈ</p>`;
      }
    }
  
Thank you again!
me Marco Pollacci Senior Frontend Developer
@ 40factory logo
Leave some feedback
qrcode slide