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
- Theming — runtime switching with di Angular theme-picker helper.
- Preference helpers — language and text size.
- Component catalog — props, ARIA, and keyboard for each component.
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.