Tutorials

Angular tutorial

Angular 20 standalone components — signal inputs, OnPush change detection, inline templates, zero NgModules, zero CSS.

What you'll build

A small contact form — labelled input, submit button, two-way binding, real keyboard and screen-reader behaviour — first bare, then styled, in about 10 minutes.

Before you start

You need git, Node 22 or later, and pnpm 10 or later. Any Angular 20 project works — Angular CLI or Analog.

Step 1 — Get the code

git clone https://github.com/LilyDesignSystem/lily-design-system-angular-headless
cd lily-design-system-angular-headless
pnpm install
pnpm test        # vitest + TestBed, all 490 components
pnpm run build-storybook

Components are flat files in components/Button.ts, TextInput.ts — each a standalone component with an inline template, plus its spec and story. The barrel index.ts re-exports every component; pnpm build produces an Angular Package Format bundle via ng-packagr.

Step 2 — Your first component

import { Component } from "@angular/core";
import { Button } from "lily-design-system-angular-headless";

@Component({
  selector: "app-save-bar",
  standalone: true,
  imports: [Button],
  template: `<lily-button (click)="save()">Save</lily-button>`,
})
export class SaveBar {
  save() { console.log("saved"); }
}

Every component declares a lily-* selector and renders the semantic element with its class hook — <button class="button" type="button"> here. Inputs are signal-based: label() for an aria-label override, className() to append classes.

Step 3 — Compose a small form

import { Component, signal } from "@angular/core";
import { Form, Field, Label, TextInput, Button } from "lily-design-system-angular-headless";

@Component({
  selector: "app-contact-form",
  standalone: true,
  imports: [Form, Field, Label, TextInput, Button],
  template: `
    <lily-form (submit)="handleSubmit($event)">
      <lily-field>
        <lily-label for="name">Full name</lily-label>
        <lily-text-input id="name" label="Full name" [(value)]="name" />
      </lily-field>
      <lily-button type="submit">Save</lily-button>
    </lily-form>
  `,
})
export class ContactForm {
  name = signal("");
}

Input-like components expose model() values, so banana-box [(value)] two-way binding just works — zoneless-compatible throughout.

Check your work: run the app and the form looks plain — exactly as intended: styling is still ahead. Press Tab: focus moves through every control, the label is announced, and submit works. The behaviour is done; the look is yours, and it's next.

Step 4 — Style it

Style the class hooks — .button, .text-input, .label, .field — in styles.css, or add a ready-made theme to the styles array in angular.json / link it in index.html:

<link rel="stylesheet" href="/themes/adobe-spectrum.css" />

Step 5 — See it all working

The Angular + Analog example app shows the full catalog with file-based routing and Vite:

git clone https://github.com/LilyDesignSystem/lily-design-system-angular-examples
cd lily-design-system-angular-examples
pnpm install && pnpm run dev

Next steps

Questions along the way? The help page is full of answers — and if a step could be clearer, tell us and we'll gladly improve the tutorial.