<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Building a Design System with Angular: Reusable components, schematics, Storybook and npm publishing

A design system is not a library of UI components: it is the shared contract between design and development that ensures visual, behavioral and accessibility consistency across all products of an organization. Building one in Angular means putting together four pieces that they are often addressed separately and poorly: reusable components with stable APIs, generators schematics that reduce adoption friction, Storybook how isolated development and documentation environment, and a publishing process on npm reliable with semantic versioning.

The measurable benefits of a mature design system are concrete: less time spent reinventing already existing components, fewer UI bugs due to divergent implementations of the same pattern, e a smaller test surface because the logic of the shared components is validated only once time rather than in every single application that consumes them. The main risk, if the design system does not have clear governance, it is the exact opposite: a library that becomes a bottleneck because every team has to wait for a centralized release for every small change.

Library Architecture: Monorepo vs Separate Repo

The first architectural decision determines everything else in the workflow. A monorepo (managed with Nx or multi-project Angular CLI workspace) holds design systems and applications consumer in the same repository: changes are instantly tested against real apps without publish an intermediate version, but the repository grows and requires tooling for incremental builds. A separate repo for the design system forces a more rigorous versioning discipline right from the start, it forces us to think of the component API as a real public contract, but it introduces latency between a change and its availability in consumers.

Criteria for Choice

CriterionMonorepoSeparate Repo
Number of consumer teams1-2 teams3+ independent teams
Iteration speedHigh, immediate feedbackSlower, requires publishing
API discipline requiredLow (you can see immediately if something breaks)High (the API is a public contract)
Tooling complexityMedium/High (Nx, build cache)Low (Angular CLI standard build)

For the naming convention, adopt a dedicated npm scope (e.g. @company/ui) from the first component, and apply Semantic Versioning strictly: patch for bugfixes without API changes, minor for new components or optional props, major for any breaking change on props existing, removing components or changing default behavior.

Component Design: Accessibility, Theming and API

Each component of the design system must follow more stringent rules than an application component any, because its impact radius is the entire organization, not a single feature.

// Component design: standalone, OnPush, API tipizzata con Signal-based inputs
@Component({
  selector: 'ds-button',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    
      @if (loading()) {  }
      
    
  `,
})
export class DsButtonComponent {
  variant = input<'primary' | 'secondary' | 'danger'>('primary');
  disabled = input(false);
  loading = input(false);
  pressed = output();

  protected variantClass = computed(() => `ds-btn ds-btn--${this.variant()}`);
}

Three non-negotiable rules for each public component: OnPush mandatory (a design system with change detection Default propagates slowdowns to each app that consumes it), API based on Signal inputs/outputs instead of exposed mutable properties directly, and zero dependencies on host app global styles — each component must be visually correct even on a blank page without external CSS, otherwise the theming becomes impossible to guarantee.

Schematics: Custom Generators to Reduce Adoption Friction

A schematic custom allows consumer teams to scaffold the correct use of a component with a single command, instead of copy-pasting examples from the documentation (which inevitably become obsolete).

// schematics/add-form-field/index.ts
export function addFormField(options: AddFormFieldOptions): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const componentPath = `${options.path}/${options.name}.component.ts`;
    const content = `
import { Component, input } from '@angular/core';
import { DsInputComponent } from '@azienda/ui/input';

@Component({
  selector: 'app-${options.name}',
  standalone: true,
  imports: [DsInputComponent],
  template: \`\`,
})
export class ${strings.classify(options.name)}Component {
  label = input.required();
  control = input.required();
}
`;
    tree.create(componentPath, content);
    context.logger.info(`✅ Creato ${componentPath}`);
    return tree;
  };
}
# Uso dello schematic da parte di un team consumer
ng generate @azienda/ui:add-form-field --name=email-field --path=src/app/features/checkout

Storybook: Setup, Addons and Stories

Storybook is the environment where components are developed, documented and visually tested in isolation from the consumer application.

# Setup iniziale in un progetto Angular esistente
npx storybook@latest init

# Addon essenziali per un design system: controls, docs automatica, a11y
npm install --save-dev @storybook/addon-a11y @storybook/addon-docs
// ds-button.stories.ts
const meta: Meta = {
  title: 'Components/Button',
  component: DsButtonComponent,
  tags: ['autodocs'],
  argTypes: {
    variant: { control: 'select', options: ['primary', 'secondary', 'danger'] },
  },
};
export default meta;

export const Primary: StoryObj = {
  args: { variant: 'primary' },
  render: (args) => ({ props: args, template: `Conferma` }),
};

The a11y addon automatically runs axe-core on each story at each build, transforming Storybook in a continuous accessibility gate instead of an occasional manual check before of the release.

Packaging and Publishing on npm

Packaging an Angular library requires ng-packagr, which generates compatible output with Ivy (partial compilation), FESM bundle and type definitions correct.

// ng-package.json
{
  "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
  "dest": "../../dist/ui",
  "lib": {
    "entryFile": "src/public-api.ts"
  }
}
// package.json della libreria — peerDependencies, non dependencies dirette
{
  "name": "@azienda/ui",
  "version": "3.4.0",
  "peerDependencies": {
    "@angular/core": "^17.0.0 || ^18.0.0",
    "@angular/common": "^17.0.0 || ^18.0.0"
  },
  "sideEffects": false
}
# Build, verifica del pacchetto e publish
npx ng-packagr -p ng-package.json
npm pack --dry-run dist/ui
npm publish dist/ui --access public

peerDependencies instead of direct dependencies is the correct choice for Angular/RxJS: Avoid each consumer app ending up with two copies of Angular in the final bundle. sideEffects: false in package.json enables consumer-side tree-shaking, like this importing just one component doesn't drag the entire library into the bundle.

Testing: Unit, Visual, E2E

// Unit test con snapshot dell'output renderizzato
it('applica la classe corretta per variant="danger"', () => {
  const fixture = TestBed.createComponent(DsButtonComponent);
  fixture.componentRef.setInput('variant', 'danger');
  fixture.detectChanges();
  expect(fixture.nativeElement.querySelector('button').className).toContain('ds-btn--danger');
});

The visual testing (Chromatic or Percy integrated with Storybook) takes a screenshot of each story at each pull request and automatically reports any pixel-diff compared to the baseline — it is the only practical way to notice an inadvertent visual regression on a used component at dozens of different points in the organization. The E2E (Cypress/Playwright) tests on design system itself should be limited to complex interaction flows (a date picker, a autocomplete with asynchronous search), not to each individual component — for most components, unit tests plus visual testing already cover the main risk.

CI/CD: Pipeline for Build, Test, Deploy and Publish

  • Build: ng-packagr more full type-check on every pull request, not just the main branch.
  • Test: unit test, visual regression and a11y check as separate, blocking steps — an a11y failure blocks the merge just like a broken test.
  • Deploy Storybook: automatic publication of a Storybook preview for each pull request, so reviewers (even non-technical ones) can visually verify each modified component before approval.
  • Publish npm: automated only on merge in main, with semantic versioning automatically calculated from commits (Conventional Commits + semantic-release), never a manual publish from laptop.

Theming and Design Tokens

Token designs are the single source of truth for colors, spacing, typography and border radii, from which automatically derive CSS custom properties, a SCSS file and a JSON export for tools design (Figma).

// tokens/colors.json — fonte di verità
{
  "color": {
    "primary": { "value": "#2563eb" },
    "danger": { "value": "#dc2626" }
  }
}
/* Output generato: CSS custom properties */
:root {
  --ds-color-primary: #2563eb;
  --ds-color-danger: #dc2626;
}

Each component consumes only CSS custom properties, never hard-coded values — that's what it is which allows a consumer application to apply a custom theme (white-label, dark mode) simply overriding root-level variables, without touching the component code.

Governance and Documentation

A design system without explicit governance quickly degrades into a collection of components inconsistent. We need a written policy for breaking changes (deprecation at least one minor version announced before removal, with runtime warning in development), a CHANGELOG automatically generated by Conventional Commits, and a clear contribution that defines who approves new components and with what criteria (duplicates a existing pattern? Is it really needed in the design system or is it specific to just one app?).

Accessibility: Checklist and Examples ARIA

  • Each interactive element can be navigated and activated by keyboard (Tab, Enter, Space), not just by mouse.
  • State aria-disabled/aria-busy correctly exposed during loading states, not just the native disabled attribute.
  • Minimum WCAG AA color contrast (4.5:1 for plain text) verified in the token designs themselves, not left to the discretion of those consuming the component.
  • Composite components (dropdown, modal, tab) implement the correct ARIA APG pattern, including role, aria-expanded, and focus trap handling where required.
<!-- Esempio: componente tab conforme ARIA APG -->
<div role="tablist" aria-label="Impostazioni account">
  <button role="tab" [attr.aria-selected]="active() === 'profile'" id="tab-profile">Profilo</button>
  <button role="tab" [attr.aria-selected]="active() === 'security'" id="tab-security">Sicurezza</button>
</div>

Performance and Bundle Size

  • Tree-shaking: separate entry points per component (@company/ui/button, @company/ui/input) instead of a single barrel file, so importing one component doesn't drag the entire library.
  • Lazy loading of heavy components (date picker with calendar, rich text editor) via @defer, not loaded in the initial bundle of consumer apps.
  • Bundle analyzer run in CI on the library itself, with a maximum size threshold per component that fails the build if exceeded.

Case Study 1: Fintech with 4 Product Teams

A fintech company with 4 independent product teams adopted a shared Angular design system on separate repo. After 6 months: average development time for a new screen reduced by 34% (less components reinvented from scratch), UI bugs reported in production reduced by 41% (same validated implementation, not 4 divergent variations of the same pattern), onboarding time of a new frontend developer reduced from 3 weeks to 8 days thanks to Storybook as documentation living.

Case Study 2: B2B Marketplace in Scale-Up Phase

A scale-up B2B marketplace that grew from 1 to 3 frontend teams in a year initially adopted a Nx monorepo for the design system, then migrated to separate repo when the third team joined. Result: application bundle size reduced by 22% after introducing per-component entry points, test coverage of shared components increased from 45% to 89%, and a 60% reduction in time spent on code review on duplicate UI implementations between teams.

Operational Checklist 30/60/90 Days

Days 1-30: Foundation

  • Setup repository, ng-packagr and first 5 core components (button, input, card, badge, spinner) — KPI: build and publish dry-run working.
  • Storybook configured with a11y addon active on each story — KPI: 0 critical a11y violations on core components.
  • Design tokens defined as single source of truth — KPI: 100% of core components use only CSS custom properties.

Days 31-60: Adoption

  • First consumer team migrated to at least 3 design system components — KPI: measurable reduction in duplicate custom CSS in that app.
  • Full CI/CD pipeline with automatic publish on merge — KPI: 0 manual publishes from laptop.
  • Visual testing active on every pull request — KPI: 0 unintentional visual regressions occurred.

Days 61-90: Scale

  • Coverage of at least 20 components covering 80% of the most common UI patterns — KPI: Documented coverage audit.
  • Formalized governance (policy breaking change, contribution process) — KPI: document published and shared with all teams.
  • At least one second consumer team onboarded — KPI: onboarding time measured and compared with the first team.

Mini-Guide 1: Creating the First Component of the Design System

Each public component starts from a minimal and typed API, not from the reproduction of each possible variant from day one.

@Component({ selector: 'ds-badge', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush,
  template: `` })
export class DsBadgeComponent { tone = input<'neutral' | 'success' | 'error'>('neutral'); }

Key Passages

  1. Define the public API (input/output) before writing the template.
  2. Apply OnPush and Signal inputs from the first commit.
  3. Add the Storybook story in the same pull request as the component.

FAQ: How many components should you start with? 5-8 core components (button, input, badge, card, spinner) are sufficient to validate the entire pipeline before scaling.

Mini-Guide 2: Writing a Custom Schematic

A schematic reduces adoption friction by translating documentation into an executable command, instead of letting each team reinterpret best practices in their own way.

ng generate @azienda/ui:add-form-field --name=email --path=src/app/checkout

Key Passages

  1. Identifies a pattern repeated manually by multiple teams.
  2. Write the Rule that generates the correct code in one command.
  3. Document the schematic in Storybook next to the component it scaffolds.

FAQ: Is a schematic worth it for just one component? Only if that component requires recurring boilerplate (form field, validation wrapper); for simple components it is not necessary.

Mini-Guide 3: Configuring Storybook with Addon a11y

npx storybook@latest init
npm install --save-dev @storybook/addon-a11y

Key Passages

  1. Activate the a11y addon in the file .storybook/main.ts.
  2. Configure the CI to fail on critical a11y violations, not just warnings.
  3. Review the results directly in the Storybook panel during development, not just in CI at the end of the work.

FAQ: Does the a11y addon replace a manual audit? No: Catch automatable violations (contrast, missing ARIA attributes), not usability issues that require testing with real users.

Mini-Guide 4: Publish the Library on npm with ng-packagr

npx ng-packagr -p ng-package.json
npm publish dist/ui --access public

Key Passages

  1. Check that peerDependencies covers all supported Angular versions.
  2. Always run npm publish --dry-run before the actual publish.
  3. Automate publishing in CI, never manually from a local non-reproducible environment.

FAQ: Is it necessary to publish at every merge? No: only when the semantic versioning calculated from the commits actually produces a new version (bugfix, feature, breaking change).

Mini-Guide 5: Export Design Token to CSS, SCSS and JSON

{ "color": { "primary": { "value": "#2563eb" } } }

Key Steps

  1. Define tokens in a neutral format (JSON) as the single source of truth.
  2. Automatically generate CSS custom properties and SCSS variables from the same file.
  3. Sync tokens with the design tool (Figma) via automated export/import, not manual copying.

FAQ: Should designers edit the JSON directly? Preferably not: they work in the design tool, and a plugin/script synchronizes the values in the token repository.

Common Mistakes to Avoid

  • Change the default change detection strategy to Default: propagates slowdowns on every app that consumes the design system.
  • Use dependencies instead of peerDependencies for Angular: causes framework duplication in consumer bundle.
  • A single barrel file that exports everything: eliminates tree-shaking and inflates the bundle even when using only one component.
  • No a11y addon in Storybook: Accessibility violations are only discovered in production, when they cost much more to fix.
  • Publish manually from laptop: introduces inconsistency between environments and makes release traceability impossible.
  • Breaking change without prior deprecation: silently break every consumer app on the next update.
  • Duplicate or hardcoded design tokens in components: Makes theming impossible to maintain and misaligns design and code over time.
  • No governance on new components: Leads to duplicates and inconsistent variations of the same pattern within a few months.

Frequently Asked Questions in Summary

What is a design system in Angular? A library of reusable, accessible, themable components, published as an npm package, that provides visual and behavioral consistency across all applications in an organization.

Is monorepo or separate repo better for a design system? Monorepo if the design system serves 1-2 teams with rapid iteration; separate repo when there are 3 or more consumer teams and a stable, versioned public API is needed.

How do I publish an Angular library to npm? With ng-packagr to generate compiled output, peerDependencies for Angular/RxJS, and npm publish automated in CI after build and test.

What is Storybook for in a design system? It is the isolated development, documentation and visual testing environment for components, with addons for interactive controls and automatic accessibility checking.

How do you manage the theming of a design system? Through design tokens exported as CSS custom properties, consumed by components instead of hardcoded values, this is how a custom theme is applied by overriding variables at the root level.

What are Angular schematics? Custom code generators that automatically scaffold the correct use of a component or pattern, reducing adoption friction compared to copying examples from the documentation.

FAQ

How many components are needed to launch the first version?

5-8 core components cover most initial use cases and allow you to validate the entire pipeline before scaling.

Do you need Nx to build an Angular design system?

No, it is especially useful in a monorepo with multiple projects, but a design system in a separate repo also works well with the Angular CLI alone.

How are breaking changes managed?

With a deprecation policy: warn at runtime at least one minor version before actual removal, documented in the CHANGELOG.

Is visual testing mandatory?

Strongly recommended over a dozen shared components: without it visual regressions are only discovered by end users.

How do you measure the success of a design system?

With objective KPIs: reduction of development time per screen, reduction of UI bugs in production, time to onboard new developers.

Do you need TypeScript strict mode for a public library?

Yes, it is strongly recommended: a library consumed by multiple teams benefits more than any other code from a strict type system.

How do you test components with complex state?

With unit tests aimed at the internal logic plus visual testing for the rendered output, reserving E2E only for the most complex interaction flows.

Should design tokens be versioned together with components?

Yes, in the same repository and in the same release cycle, because a change of token is in effect a change of the visual API.

How do you prevent each team from creating divergent variations of the same component?

With a clear contribution process that requires you to verify the existence of a similar pattern before creating a new one.

How long does it take to build a mature design system?

Typically 3-6 months for a robust library with 20+ components, governance and full CI/CD pipeline, depending on the size of the dedicated team.

Conclusion

A well-built Angular design system is not a "one-off" project: it is an internal product with i its users (the developers of the consumer teams), its roadmap and its governance. Components OnPush with typed API, Storybook as living documentation, ng-packagr for packaging correct and a CI/CD pipeline that automates testing, visual regression and publish are the elements that distinguish a truly adopted design system from a library of components abandoned after a few months.

Want a printable checklist or assessment of your existing component library? Request a technical audit: in a few hours of analysis it is possible to identify accessibility gaps, performance and governance priorities for your design system.

💬 Reader notes

0 notes

Write a note

Share your opinion, a suggestion or a compliment

Latest notes

No notes yet. Be the first to comment!