<link rel="stylesheet" href="assets/fonts/inter.css"> Angular Design System: Tokens, Components & Theming… | Viral Patel Studio Skip to main content

Angular Design System: Tokens, Components & Theming (2026)

Sep 15, 2025By Viral Patel

Build an Angular design system in 2026: CSS tokens, signal-based standalone components, Material 3 theming, Storybook docs and npm publishing, step by step.

Angular Design System: Tokens, Components & Theming (2026)

An Angular design system is four layers working together: design tokens as CSS custom properties, standalone components that read only those tokens, a theming layer that swaps token values, and a publishable library with documentation and versioning. Build them in that order and every later change stays cheap.

learn about Angular Development

angular design system

design systems

angular development

Angular Storybook (2026): Setup, Docs & Design System Guide

Whether you call them Angular systems or a shared UI kit, the principles are the same. This guide is the 2026 rewrite of my original walkthrough. The older version used constructor-injected token objects, @Input() decorators and a module-based export, which is not how you would start a library today. Here is the current approach, checked against the official Angular, Angular Material and ng-packagr documentation.

If you only need the short version of each layer, read the bold lead-ins. If you are building, follow the sections in order.


Why Most Angular Design Systems Stall After Six Months

Teams usually start with components. A button appears, then an input, then a dialog, each with its own hard-coded colors and spacing. By the time a rebrand or a dark mode request arrives, the values are scattered across hundreds of files.

The fix is not more components. It is putting a token layer underneath them before the second component exists. Tokens are the contract between design and code, and they are the only part of the system that every other layer depends on.

A second failure mode is treating the system as a one-off project. A design system is a product with consumers, releases and a changelog. If nobody owns versioning and documentation, adoption stops. For the organizational case, see my post on the benefits of building a design system and the broader design systems guide.

Get the layers right, in the right order, and the system pays for itself with every screen it touches.


Section 1: Plan the Architecture and Library Structure

Start with the smallest structure that can be published. A single Angular workspace with one library project is enough. Generate it with ng generate library and keep the application that consumes it in the same workspace so you can test changes immediately.

Recommended folder layout

projects/
  ui/
    ng-package.json
    package.json
    src/
      public-api.ts
      lib/
        tokens/        raw and semantic CSS variables
        button/
        input/
        dialog/
    testing/           optional secondary entry point
  playground/          the consuming app

Entry points. The Angular Package Format lets a package have one primary entry point and any number of secondary ones, such as @your-org/ui/testing. The official format documentation recommends a single entry point for libraries with one logical purpose, so add secondary entry points only when consumers genuinely want to import parts separately.

Build tooling. According to the Angular library guide, libraries are built by ng-packagr, and a library project in angular.json uses the @angular/build:ng-packagr builder. The older @angular-devkit/build-angular:ng-packagr builder that appeared in my previous edition is the previous name.

One practical rule from the same guide. An application that depends on a library should use TypeScript path mappings that point to the built library, not to its source files, because the application and library build systems compile differently.

When to adopt a monorepo tool

Add Nx when you have several libraries, several apps, or ownership boundaries you want to enforce. I cover that setup in the Nx monorepo guide for Angular design systems. Before that point, plain Angular CLI workspaces are simpler and faster to reason about.


Section 2: Build the Token Layer with CSS Custom Properties

Angular does not ship a token system, and it does not need one. CSS custom properties are inherited, themeable at runtime and readable from any component stylesheet, which makes them the right primitive.

Use three tiers

  1. Primitive tokens hold raw values: a blue ramp, a spacing scale, a font stack.
  2. Semantic tokens give those values meaning: --ds-color-action, --ds-color-surface.
  3. Component tokens (optional) let one component expose a few overridable values.

Components should read semantic tokens only. That single rule is what makes theming a pure CSS concern.

/* projects/ui/src/lib/tokens/tokens.css */
:root {
  /* primitive */
  --ds-blue-600: #2563eb;
  --ds-blue-700: #1d4ed8;
  --ds-gray-50: #f8fafc;
  --ds-gray-900: #0f172a;
  --ds-space-2: 0.5rem;
  --ds-space-4: 1rem;
  --ds-radius-md: 0.5rem;

  /* semantic */
  --ds-color-action: var(--ds-blue-600);
  --ds-color-action-hover: var(--ds-blue-700);
  --ds-color-on-action: #ffffff;
  --ds-color-surface: var(--ds-gray-50);
  --ds-color-text: var(--ds-gray-900);
  --ds-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ds-color-action) 40%, transparent);
}

:root.ds-dark {
  --ds-color-surface: #0b1220;
  --ds-color-text: #e2e8f0;
}

Dark mode here is a handful of redefined semantic variables, not a second stylesheet. Because components never reference --ds-blue-600 directly, a rebrand edits one block.

Naming matters more than values. Name tokens by intent (--ds-color-action), not by appearance (--ds-color-blue). Intent names survive a rebrand, a dark theme and a high-contrast mode, while appearance names turn misleading the first time the brand color changes. Prefix every variable with your system's short name so consumers can tell design system tokens from their own, and so a global stylesheet never collides with third-party CSS.

Keep the token set small at first. Color, spacing, radius, typography and focus ring cover most of what a first release needs, and each extra token is something you must document, test and keep stable.

Keep Figma and code in sync

Hand-written token files drift from the design file. The robust fix is to treat a JSON export from Figma variables as the source of truth and generate the CSS from it. My guide to design tokens with Angular, Figma variables and Style Dictionary walks through that pipeline end to end.


Section 3: Build Components the Modern Angular Way

Current Angular components are standalone, use signal-based inputs and read tokens through CSS. There is no module to declare them in and no token object to inject.

A token-driven button

import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';

type ButtonVariant = 'primary' | 'secondary' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';

@Component({
  selector: 'button[ds-button]',
  template: '<ng-content />',
  styleUrl: './button.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'ds-button',
    '[attr.data-variant]': 'variant()',
    '[attr.data-size]': 'size()',
    '[attr.aria-busy]': 'loading() || null',
    '[disabled]': 'disabled() || loading()',
  },
})
export class DsButton {
  readonly variant = input<ButtonVariant>('primary');
  readonly size = input<ButtonSize>('md');
  readonly disabled = input(false);
  readonly loading = input(false);

  protected readonly isInteractive = computed(() => !this.disabled() && !this.loading());
}
/* button.css */
.ds-button {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-4);
  border: 0;
  border-radius: var(--ds-radius-md);
  background: var(--ds-color-action);
  color: var(--ds-color-on-action);
  cursor: pointer;
}

.ds-button:hover:not(:disabled) {
  background: var(--ds-color-action-hover);
}

.ds-button:focus-visible {
  outline: none;
  box-shadow: var(--ds-focus-ring);
}

.ds-button:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

Several choices here are deliberate.

  • Attribute selector on a native element. button[ds-button] keeps the real <button>, so keyboard, form and screen reader behavior come free. Wrapping a button in a custom element is the most common accessibility mistake in design systems.
  • input() instead of @Input(). Signal inputs are read as functions and compose with computed(), which suits a zoneless, signal-first app. My guide to Angular signals for design system components goes deeper.
  • OnPush. Angular 22 made OnPush the default for components that do not set a strategy, according to coverage of the release from Angular Architects and Angular.love. Setting it explicitly keeps the library correct for consumers on older versions.
  • Host bindings in the decorator. The host object replaces @HostBinding and keeps state attributes next to the component definition.

Do not rebuild behavior you can reuse

Overlays, focus traps, drag and drop and virtual scrolling already exist in the Angular CDK. For keyboard-heavy widgets such as tabs, menus, listboxes and accordions, the Angular team also publishes @angular/aria, which its documentation describes as headless directives that handle keyboard interaction, ARIA attributes, focus management and screen reader support, while you provide the HTML, CSS and logic. That is an ideal base for a custom design system. Read my Angular CDK explainer to see which primitives to reuse, and confirm the current stability status of @angular/aria in the release notes of your Angular version before depending on it.


Section 4: Theming, Including Angular Material 3

You have two honest options: theme your own components with the token layer above, or build on Angular Material and theme it.

Theme your own components

Theme switching is a class or prefers-color-scheme rule that redefines semantic tokens, as in the dark block earlier. Add color-scheme: light dark on the root so native controls follow.

Theme Angular Material with mat.theme()

The current Angular Material theming guide uses one mixin that takes a map of color, typography and density, and outputs Material Design 3 system tokens as CSS variables with the --mat-sys- prefix.

@use '@angular/material' as mat;

html {
  color-scheme: light dark;

  @include mat.theme((
    color: mat.$violet-palette,
    typography: Roboto,
    density: 0
  ), $overrides: (
    primary-container: orange,
  ));
}

The official guide documents several details worth knowing:

  • Density accepts integers from 0 to -5, and each step down reduces affected sizes by 4px. The guide warns that dense settings can reduce accessibility for assistive technology users.
  • Individual system tokens can be redefined through the $overrides argument, or through mat.theme-overrides.
  • Every component also has its own overrides mixin, which validates that token names are spelled correctly.

Bridge the two worlds. If you mix your components with Material, map your semantic tokens to Material's. For example, set --ds-color-action: var(--mat-sys-primary) so one theme drives both.

For a wider view on choosing a base library, see my comparison of Angular Material and PrimeNG.


Section 5: Package the Library with ng-packagr

A design system is only useful when other teams can install it. Configure the library so ng-packagr emits the Angular Package Format.

{
  "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
  "dest": "../../dist/ui",
  "lib": {
    "entryFile": "src/public-api.ts"
  }
}
// projects/ui/src/public-api.ts
export * from './lib/button/button';
export * from './lib/input/input';
export * from './lib/dialog/dialog';
{
  "name": "@your-org/ui",
  "version": "1.0.0",
  "sideEffects": false,
  "peerDependencies": {
    "@angular/common": ">=22.0.0 <23.0.0",
    "@angular/core": ">=22.0.0 <23.0.0"
  },
  "dependencies": {
    "tslib": "^2.3.0"
  }
}

Ship the tokens too. The Angular library guide says additional assets such as Sass mixins or pre-compiled CSS must be added manually to the conditional exports in the primary entry point's package.json; ng-packagr merges hand-written exports with the generated ones. Export tokens.css that way so consumers can import it directly.

Declare side effects. The Angular Package Format documentation says most Angular packages should declare "sideEffects": false, and the pattern above does that. If your token stylesheet is imported for its effect, check how your bundler treats CSS imports before keeping that flag.

Peer dependencies. Keep Angular in peerDependencies so consumers choose the framework version. My step-by-step guide to publishing an Angular component library to npm covers registries, provenance and release automation in detail.


Section 6: Document Everything in Storybook

Undocumented components are not part of the system. Storybook gives each component a living page with controls, variants and accessibility checks.

import type { Meta, StoryObj } from '@storybook/angular';
import { DsButton } from './button';

const meta: Meta<DsButton> = {
  title: 'Components/Button',
  component: DsButton,
  tags: ['autodocs'],
  argTypes: {
    variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] },
    size: { control: 'select', options: ['sm', 'md', 'lg'] },
  },
};
export default meta;

type Story = StoryObj<DsButton>;

export const Primary: Story = { args: { variant: 'primary', size: 'md' } };
export const Ghost: Story = { args: { variant: 'ghost' } };

Write one story per meaningful state, include a dark-theme story, and document do and don't usage next to the code. Nx documents Storybook Autodocs and Compodoc as the way to infer argument types for Angular projects. For the full documentation workflow, including accessibility and visual testing, read my Storybook guide for Angular design systems.


Section 7: Accessibility and Testing Built In

Accessibility belongs in the component contract, not in a later audit. Every component in an Angular design system should ship with keyboard support, visible focus, correct roles and sufficient contrast, so product teams inherit it by default.

Test behavior, not markup. Angular Material exposes component harnesses for convenient, meaningful tests, and you can write the same kind of harness for your own components. A harness hides internal DOM details, so a refactor does not break every consumer test.

Automate the checks you can. Run an accessibility rule engine in Storybook and in CI, and add a visual regression step for each theme. Automated tools catch only part of the problem, so keep a manual keyboard and screen reader pass in the release checklist for complex widgets such as dialogs and comboboxes.

Make the accessible path the easy path. Provide a focus ring token, a reduced-motion rule and a documented pattern for labels and error messages. If using the component correctly takes more effort than using it incorrectly, teams will bypass it. For a deeper checklist, see my web accessibility guide.

Treat these checks as release gates. A component that fails keyboard navigation or contrast in any shipped theme does not get published, no matter how good it looks in the design file.


Section 8: Versioning, Governance and Adoption

Treat the design system as a product with a release process.

  1. Semantic versioning. Breaking changes bump the major version, new components bump the minor, fixes bump the patch.
  2. A changelog. Every release explains what changed and how to migrate.
  3. A deprecation policy. Mark an API deprecated in one minor release, and remove it in a later major.
  4. A contribution path. Say how a product team proposes a new component, and who reviews it.
  5. Usage checks. Lint for hard-coded colors and spacing in consuming apps, so tokens are actually used.

Start with three or four components that appear on every screen: button, input, select and dialog. Add the next one only when two products ask for it. A small system that people trust beats a large one nobody maintains.


When NOT to Build Your Own Angular Design System

A custom system is not free, so be honest about the cost.

  • One small product, one team. Use Angular Material or another maintained library and theme it. You will ship faster and inherit accessibility work.
  • No design resource. Without someone owning the visual language, a custom system becomes a pile of inconsistent components.
  • No one to maintain it. Components need accessibility fixes, framework upgrades and bug triage. If no one has that time, adopt an existing library.
  • Heavy data components needed immediately. Grids and charts are expensive to build; start from a library and add your own components on top.

Build your own when you have several products, a distinct brand and a team that can commit to releases.


Conclusion: Tokens First, Components Second, Releases Always

The pattern that works in 2026 is simple: CSS custom properties for tokens, standalone signal-based components that read them, theming by redefining semantic variables, and a library packaged by ng-packagr with Storybook documentation and semantic versioning.

Reuse the CDK and @angular/aria for behavior, theme Angular Material with mat.theme() if you adopt it, and resist the urge to grow the component list ahead of real demand.

A design system is judged by how little your next product team has to think about it, so make the tokens right and the rest follows.