Angular tutorial

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

Wetin you go build

Small contact form — input wey get label, submit button, two-way binding, real keyboard and screen-reader behaviour — first bare, then styled, for about 10 minutes.

Before you start

You need git, Node 22 or newer, and pnpm 10 or newer. Any Angular 20 project dey work — Angular CLI or Analog.

Step 1 — Get di code

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

Di components na flat files for components/ — Button.ts, TextInput.ts — each one na standalone component wey get inline template, plus im spec and story. Di barrel index.ts dey re-export every component; pnpm build dey produce Angular Package Format bundle through ng-packagr.

Step 2 — Your first component

import { Component } from "@angular/core";
import { Button } from "@lilydesignsystem/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 dey declare lily-* selector and e dey render di semantic element with im class hook — <button class="button" type="button"> here. Di inputs dey signal-based: label() for aria-label override, className() to add classes.

Step 3 — Compose small form

import { Component, signal } from "@angular/core";
import { Form, Field, TextInput, Button } from "@lilydesignsystem/angular-headless";

@Component({
  selector: "app-contact-form",
  standalone: true,
  imports: [Form, Field, TextInput, Button],
  template: `
    <lily-form label="Contact form" (submit)="handleSubmit($event)">
      <lily-field label="Full name" [required]="true">
        <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("");
}

lily-field dey take di visible label as im own label input instead of nested label component — pass am once, for di field, and di matching lily-text-inputfor label go provide di accessible name. Input-like components dey expose model() values, so banana-box [(value)] two-way binding just dey work — zoneless-compatible from start to finish.

Verified 2026-08-29: earlier draft nest separate Label component inside lily-field, wey render two labels (one empty) because lily-field already dey render im own from di label input — dem catch am by checking di component source. Di contact-form composed route na di same pattern, wey real e2e and axe tests don exercise.

Check your work: run di app and di form go look plain — exactly as dem plan am: di styling still dey come. Press Tab: focus go move through every control, di label go dey announced, and submit dey work. Di behaviour don finish; di look na yours and e be next.

Step 4 — Style am

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

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

Step 5 — See everything dey work

Di Angular + Analog example app show di full catalog with file-based routing and Vite:

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

Next steps

You get questions as you dey go? Di help page full of answers — and if any step fit clearer, tell us and we go gladly improve di tutorial.