Ready-to-use Angular starter for building a design system: a publishable component library (standalone, zoneless, signals) and a Storybook workshop, with every best practice and tool already wired up.
The starter ships a documented set of design tokens (neutral zinc scale, light and dark via light-dark()) and example components (button, input, badge, textarea, field) showing the conventions: signal inputs, host class bindings, Tailwind utilities consuming the tokens, stories with interaction tests, and unit tests with enforced coverage thresholds.
It is the design system counterpart of angular-starter-web, which leaves the choice of UI library open.
| Repository | What it generates |
|---|---|
| angular-starter-web | An Angular web application connected to a backend |
| angular-starter-ui (this one) | An Angular design system: component library and Storybook |
| java-starter | A Spring Boot backend in hexagonal architecture |
Create an application from any of them with starter-generator.
| Tool | Role |
|---|---|
| Angular + ng-packagr | Framework and library build (Angular Package Format) |
Storybook (@storybook/angular-vite) |
Component workshop: autodocs, a11y addon, theme switcher |
| Tailwind CSS | Utility-first CSS consuming the design tokens |
| ESLint + angular-eslint + eslint-plugin-storybook | Lint for TypeScript code, templates and stories |
| Prettier + sort-imports + tailwindcss | Code formatting, import ordering and class sorting |
| Vitest + Testing Library | Unit tests (Angular's default runner) |
| Storybook Vitest addon | Runs every story, its play function and axe in Chromium |
| Sheriff | Enforces module boundaries (components stay isolated) |
| Husky + lint-staged | Git hooks (format + lint on commit) |
| commitlint | Commit message validation (Conventional Commits) |
| GitHub Actions | CI: format, lint, unit tests, builds, interaction tests |
Generate a design system from this starter:
starter-generator portfolio-ui --starter uiThen run it. Prerequisites: Node (supported versions in engines of package.json) and pnpm.
pnpm install # installs dependencies
pnpm start # Storybook on http://localhost:6006Storybook is the development surface: components are built and reviewed through their stories, there is no demo application.
| Script | Description |
|---|---|
pnpm start |
Storybook dev server on http://localhost:6006 |
pnpm run build |
Builds the library into dist/ui (Angular Package Format) |
pnpm run build:dev |
Development build of the library |
pnpm run build-storybook |
Builds the static Storybook into storybook-static/ |
pnpm test |
Unit tests (Vitest) |
pnpm run test:coverage |
Unit tests with coverage report and thresholds |
pnpm run test:storybook |
Story tests (play functions and axe) in Chromium, light and dark |
pnpm run lint |
Lint (ESLint) |
pnpm run lint:fix |
Lint with automatic fixes |
pnpm run format |
Format the whole project (Prettier) |
pnpm run format:check |
Check formatting without modifying anything |
.storybook/ # Storybook config: main.ts, preview.ts (theme switcher), theme.ts, preview.css
projects/ui/
|-- ng-package.json # ng-packagr config (root entry, shipped assets)
|-- package.json # Library manifest (rename before publishing)
|-- CHANGELOG.md # One `## X.Y.Z` section per release, shipped in the package
|-- README.md # npm page of the published package
|-- styles/
| `-- tokens.css # Design tokens, shipped as dist/ui/styles/tokens.css
|-- docs/ # Storybook "Foundations" MDX pages (overview, colors, typography)
|-- button/ # One entry point per component: ng-package.json, index.ts,
|-- input/ # button.ts + button.spec.ts + button.stories.ts
|-- badge/
|-- control/ # UI_CONTROL token shared by input and field
|-- field/ # ui-field: label, hint and error around a control
`-- src/
`-- public-api.ts # Exports nothing on purposeEach component lives in its own folder with its spec and stories co-located. Dependency rules, enforced at lint time by Sheriff (sheriff.config.ts): components cannot import each other unless depRules grants it, and shared building blocks get their own module. Each folder is a module whose index.ts is its barrel, and files a module wants to keep private go in an internal/ subdirectory. Each folder is also its own entry point (ng-package.json + index.ts) so a consumer pulls in only the components it imports; public-api.ts exports nothing and must never re-export an entry.
All colors come from projects/ui/styles/tokens.css, a pure token sheet (no resets, no element styles) shipped with the package. Tokens follow the shadcn/ui semantic convention on a neutral zinc scale.
Each token declares its light and dark value with light-dark(); the active scheme follows the OS preference unless the host forces one:
<html data-theme="dark"></html>The Storybook toolbar theme switcher does exactly that, so every story can be reviewed in both themes. Components consume tokens through Tailwind arbitrary-value utilities (bg-(--primary), border-(--border), ...): retheming means editing one CSS file. The tokens are documented in Storybook under Foundations.
Five example components demonstrate the conventions (prefix ui, signal inputs, host class bindings):
| Component | Selector | API |
|---|---|---|
| Button | button[ui-button], a[ui-button] |
variant: primary | secondary | destructive | ghost, size: sm | md | lg |
| Input | input[uiInput] |
Styled native input: works with any forms API without plumbing |
| Badge | ui-badge |
variant: primary | secondary | destructive | outline |
| Textarea | textarea[uiTextarea] |
Styled native textarea, same contract as the input |
| Field | ui-field |
label, labelHidden, hint, error: wires the projected input or textarea |
Button and input attach to native elements (Material-style attribute selectors), keeping native semantics, accessibility and forms compatibility for free:
<button ui-button variant="destructive" size="sm">Delete</button>
<input uiInput type="email" placeholder="you@example.com" />
<ui-badge variant="outline">Draft</ui-badge>The live Storybook is at https://joanroucoux.github.io/angular-starter-ui/.
- Autodocs: every story is tagged
autodocsglobally (.storybook/preview.ts); each component gets a generated docs page from its stories andargTypes. - Accessibility: the a11y addon runs axe checks on every story, and a violation fails
test:storybook. - Theming: the toolbar switcher (system, light, dark) toggles
data-themeon<html>viawithThemeByDataAttribute, matching the tokens'light-dark()contract. The Storybook chrome and the Docs pages follow the OS preference with a theme built from the tokens (.storybook/theme.ts). - Interaction tests: stories declare
playfunctions (imports fromstorybook/test); the Vitest addon runs every story in Chromium, once in light and once in dark, in CI (pnpm run test:storybook). - Foundations: MDX pages in projects/ui/docs give the Overview landing page (installation, themes, contributing), the color tokens (light and dark swatches) and typography.
- Docs pages: every component carries a description (what it is, when to use it, when not to, accessibility) and a description per control.
Releases are made by tag with npm trusted publishing and Storybook is deployed to GitHub Pages: see the Release section of AGENTS.md.
The publishable artifact is built from projects/ui into dist/ui. To rename the package (for example to a scope), change three places together: name in projects/ui/package.json, the paths key in tsconfig.json, and every quoted cross-entry import ('angular-starter-ui/...' in specs and stories); renaming only package.json breaks the build. The root package.json version is the starter's own version (managed by release-please), the library version is managed in projects/ui/package.json.
In the consuming app:
- Install the package and its peer dependencies (
@angular/common,@angular/core). - Import the token sheet once, e.g. in
styles.css:@import 'angular-starter-ui/styles/tokens.css';
- Tailwind consumers: the library's templates are inside
node_modules, which Tailwind does not scan by default. Add a@sourceso the utilities used by the components are generated:Non-Tailwind consumers only need the tokens import: component class names would then need to be covered by your own CSS strategy; in that case prefer forking the components into your design system, which is what a starter is for.@import 'tailwindcss'; @source '../node_modules/angular-starter-ui';
The library exports each component's own types so you never recreate them. Every variant/size union comes with a matching as const tuple (the single source of truth the type is derived from), so you get both the type and the runtime list:
import { BUTTON_VARIANTS, type ButtonVariant } from 'angular-starter-ui/button';
// Type your own props with the library's union...
function setTone(variant: ButtonVariant) {
/* ... */
}
// ...and iterate the values without hardcoding them (selects, validation, docs).
const options = BUTTON_VARIANTS.map((variant) => ({ label: variant, value: variant }));Exported today: ButtonVariant / BUTTON_VARIANTS, ButtonSize / BUTTON_SIZES (from /button), BadgeVariant / BADGE_VARIANTS (from /badge), and from /control the UI_CONTROL token with the UiControl and UiControlError types.
- Unit tests use Angular Testing Library (
render,screen,userEvent): querying by role or label asserts accessibility for free. The jest-dom matchers are registered in projects/ui/src/test-setup.ts. - Coverage is 100% on branches and lines, 97.27% on statements and 93.33% on functions (the
contentChildquery function infieldcounts as never called); the CI thresholds (statements 85, branches 80, functions 70, lines 85 in angular.json) are a floor. Stories are excluded from coverage. - Interaction tests live in the stories themselves (
playfunctions) and run in a real browser through the Storybook Vitest addon, against the stories sources (no built Storybook needed).
- On commit: lint-staged formats and lints the staged files; commitlint enforces Conventional Commits (
feat: ...,fix: ..., ...). - In CI (.github/workflows/ci.yml): format check, lint, unit tests, library build, Storybook build and interaction tests on every push/PR.
- Code conventions: see the Angular style guide. In short: standalone components (
OnPushchange detection is the Angular default since v22, no need to declare it), signals (input(),computed()),inject(), private#properties, control flow (@if,@for), no.component/.directivesuffix in file names. - Config files use ESM
.mjswhere the tool supports it: eslint.config.mjs, prettier.config.mjs, commitlint.config.mjs.
See CONTRIBUTING.md for the contribution workflow and CODE_OF_CONDUCT.md for community guidelines. To report a vulnerability, see SECURITY.md.
This project is licensed under MIT.
