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

Costruire un Design System con Angular: Componenti, schematics, Storybook e publishing su npm

Un design system non è una libreria di componenti UI: è il contratto condiviso tra design e sviluppo che garantisce coerenza visiva, comportamentale e di accessibilità su tutti i prodotti di un'organizzazione. Costruirne uno in Angular significa mettere insieme quattro pezzi che spesso vengono affrontati separatamente e male: componenti riutilizzabili con API stabili, generatori schematics che riducono l'attrito di adozione, Storybook come ambiente di sviluppo e documentazione isolato, e un processo di publishing su npm affidabile con versioning semantico.

I benefici misurabili di un design system maturo sono concreti: meno tempo speso a reinventare componenti già esistenti, meno bug di UI dovuti a implementazioni divergenti dello stesso pattern, e una superficie di test più piccola perché la logica dei componenti condivisi è validata una sola volta invece che in ogni singola applicazione che li consuma. Il rischio principale, se il design system non ha una governance chiara, è l'esatto opposto: una libreria che diventa collo di bottiglia perché ogni team deve aspettare un rilascio centralizzato per ogni piccola modifica.

Architettura della Libreria: Monorepo vs Repo Separato

La prima decisione architetturale determina tutto il resto del workflow. Un monorepo (gestito con Nx o con gli Angular CLI workspace multi-progetto) tiene design system e applicazioni consumer nello stesso repository: i cambiamenti si testano istantaneamente contro le app reali senza pubblicare una versione intermedia, ma il repository cresce e richiede tooling per build incrementali. Un repo separato per il design system forza una disciplina di versioning più rigorosa fin da subito, obbliga a pensare l'API dei componenti come un vero contratto pubblico, ma introduce latenza tra una modifica e la sua disponibilità nei consumer.

Criteri di Scelta

CriterioMonorepoRepo Separato
Numero di team consumer1-2 team3+ team indipendenti
Velocità di iterazioneAlta, feedback immediatoPiù lenta, richiede pubblicazione
Disciplina API richiestaBassa (si vede subito se rompe qualcosa)Alta (l'API è un contratto pubblico)
Complessità di toolingMedia/alta (Nx, build cache)Bassa (build standard Angular CLI)

Per la naming convention, adotta uno scope npm dedicato (es. @azienda/ui) fin dal primo componente, e applica Semantic Versioning in modo rigoroso: patch per bugfix senza cambi di API, minor per nuovi componenti o props opzionali, major per ogni breaking change su props esistenti, rimozione di componenti o cambi di comportamento di default.

Component Design: Accessibilità, Theming e API

Ogni componente del design system deve seguire regole più stringenti di un componente applicativo qualsiasi, perché il suo raggio d'impatto è l'intera organizzazione, non una singola 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()}`);
}

Tre regole non negoziabili per ogni componente pubblico: OnPush obbligatorio (un design system con change detection Default propaga rallentamenti a ogni app che lo consuma), API basata su Signal inputs/outputs invece di proprietà mutabili esposte direttamente, e zero dipendenze da stili globali dell'app host — ogni componente deve essere visivamente corretto anche in una pagina vuota senza CSS esterno, altrimenti il theming diventa impossibile da garantire.

Schematics: Generatori Custom per Ridurre l'Attrito di Adozione

Uno schematic custom permette ai team consumer di scaffoldare l'uso corretto di un componente con un singolo comando, invece di copiare-incollare esempi dalla documentazione (che inevitabilmente diventano obsoleti).

// 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, Addon e Stories

Storybook è l'ambiente dove i componenti vengono sviluppati, documentati e testati visivamente in isolamento dall'applicazione consumer.

# 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` }),
};

L'addon a11y esegue automaticamente axe-core su ogni story ad ogni build, trasformando Storybook in un gate di accessibilità continuo invece che in un controllo manuale occasionale prima del rilascio.

Packaging e Publishing su npm

Il packaging di una libreria Angular richiede ng-packagr, che genera output compatibili con Ivy (partial compilation), FESM bundle e type definitions corrette.

// 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 invece di dependencies dirette è la scelta corretta per Angular/RxJS: evita che ogni app consumer finisca con due copie di Angular nel bundle finale. sideEffects: false nel package.json abilita il tree-shaking lato consumer, così importare un solo componente non trascina l'intera libreria nel 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');
});

Il visual testing (Chromatic o Percy integrati con Storybook) cattura uno screenshot di ogni story ad ogni pull request e segnala automaticamente ogni pixel-diff rispetto al baseline — è l'unico modo pratico per accorgersi di una regressione visiva involontaria su un componente usato in decine di punti diversi dell'organizzazione. I test E2E (Cypress/Playwright) sul design system stesso vanno limitati ai flussi di interazione complessi (un date picker, un autocomplete con ricerca asincrona), non a ogni singolo componente — per la maggior parte dei componenti, unit test più visual testing coprono già il rischio principale.

CI/CD: Pipeline per Build, Test, Deploy e Publish

  • Build: ng-packagr più type-check completo su ogni pull request, non solo sul branch principale.
  • Test: unit test, visual regression e controllo a11y come step separati e bloccanti — un fallimento a11y blocca il merge esattamente come un test rotto.
  • Deploy Storybook: pubblicazione automatica di una preview Storybook per ogni pull request, così i reviewer (anche non tecnici) possono verificare visivamente ogni componente modificato prima dell'approvazione.
  • Publish npm: automatizzato solo sul merge in main, con versioning semantico calcolato automaticamente dai commit (Conventional Commits + semantic-release), mai un publish manuale da laptop.

Theming e Design Tokens

I design token sono la fonte di verità unica per colori, spaziature, tipografia e raggi di bordo, da cui derivano automaticamente CSS custom properties, un file SCSS e un export JSON per strumenti di 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;
}

Ogni componente consuma esclusivamente le CSS custom properties, mai valori hardcoded — questo è ciò che permette a un'applicazione consumer di applicare un tema custom (white-label, dark mode) semplicemente sovrascrivendo le variabili a livello di root, senza toccare il codice dei componenti.

Governance e Documentazione

Un design system senza governance esplicita degrada rapidamente in un insieme di componenti inconsistenti. Serve una policy scritta per le breaking changes (deprecazione annunciata almeno una minor version prima della rimozione, con warning a runtime in sviluppo), un CHANGELOG generato automaticamente dai Conventional Commits, e un processo di contribuzione chiaro che definisce chi approva nuovi componenti e con quali criteri (duplica un pattern esistente? Serve davvero nel design system o è specifico di una sola app?).

Accessibilità: Checklist ed Esempi ARIA

  • Ogni elemento interattivo è navigabile e attivabile da tastiera (Tab, Enter, Space), non solo da mouse.
  • Stato aria-disabled/aria-busy esposto correttamente durante stati di caricamento, non solo l'attributo disabled nativo.
  • Contrasto colore minimo WCAG AA (4.5:1 per testo normale) verificato nei design token stessi, non lasciato alla discrezione di chi consuma il componente.
  • Componenti composti (dropdown, modal, tab) implementano il pattern ARIA APG corretto, incluso role, aria-expanded, e gestione del focus trap dove richiesto.
<!-- 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 e Bundle Size

  • Tree-shaking: entry point separati per componente (@azienda/ui/button, @azienda/ui/input) invece di un unico barrel file, così importare un componente non trascina l'intera libreria.
  • Lazy loading dei componenti pesanti (date picker con calendario, rich text editor) tramite @defer, non caricati nel bundle iniziale delle app consumer.
  • Bundle analyzer eseguito in CI sulla libreria stessa, con una soglia massima di dimensione per componente che fallisce la build se superata.

Case Study 1: Fintech con 4 Team Prodotto

Un'azienda fintech con 4 team prodotto indipendenti ha adottato un design system Angular condiviso su repo separato. Dopo 6 mesi: tempo medio di sviluppo di una nuova schermata ridotto del 34% (meno componenti reinventati da zero), bug di UI segnalati in produzione ridotti del 41% (stessa implementazione validata, non 4 varianti divergenti dello stesso pattern), tempo di onboarding di un nuovo sviluppatore frontend ridotto da 3 settimane a 8 giorni grazie a Storybook come documentazione vivente.

Case Study 2: Marketplace B2B in Fase di Scale-Up

Un marketplace B2B in scale-up, passato da 1 a 3 team frontend in un anno, ha adottato inizialmente un monorepo Nx per il design system, poi migrato a repo separato quando il terzo team è entrato. Risultato: dimensione del bundle applicativo ridotta del 22% dopo l'introduzione di entry point per-componente, copertura test dei componenti condivisi passata dal 45% al 89%, e riduzione del 60% del tempo speso in code review su implementazioni UI duplicate tra i team.

Checklist Operativa 30/60/90 Giorni

Giorni 1-30: Fondamenta

  • Setup repository, ng-packagr e primi 5 componenti core (button, input, card, badge, spinner) — KPI: build e publish dry-run funzionanti.
  • Storybook configurato con addon a11y attivo su ogni story — KPI: 0 violazioni a11y critiche sui componenti core.
  • Design token definiti come fonte di verità unica — KPI: 100% dei componenti core usano solo CSS custom properties.

Giorni 31-60: Adozione

  • Primo team consumer migrato su almeno 3 componenti del design system — KPI: riduzione misurabile di CSS custom duplicato in quell'app.
  • Pipeline CI/CD completa con publish automatico su merge — KPI: 0 publish manuali da laptop.
  • Visual testing attivo su ogni pull request — KPI: 0 regressioni visive non intenzionali mergiate.

Giorni 61-90: Scala

  • Copertura di almeno 20 componenti coprendo l'80% dei pattern UI più comuni — KPI: audit di copertura documentato.
  • Governance formalizzata (policy breaking change, processo di contribuzione) — KPI: documento pubblicato e condiviso con tutti i team.
  • Almeno un secondo team consumer onboardato — KPI: tempo di onboarding misurato e confrontato col primo team.

Mini-Guida 1: Creare il Primo Componente del Design System

Ogni componente pubblico parte da un'API minimale e tipizzata, non dalla riproduzione di ogni possibile variante fin dal primo giorno.

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

Passaggi Chiave

  1. Definisci l'API pubblica (input/output) prima di scrivere il template.
  2. Applica OnPush e Signal inputs fin dal primo commit.
  3. Aggiungi la story Storybook nello stesso pull request del componente.

FAQ: Da quanti componenti conviene partire? 5-8 componenti core (button, input, badge, card, spinner) sono sufficienti per validare l'intera pipeline prima di scalare.

Mini-Guida 2: Scrivere uno Schematic Custom

Uno schematic riduce l'attrito di adozione traducendo la documentazione in un comando eseguibile, invece di lasciare che ogni team reinterpreti le best practice a modo suo.

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

Passaggi Chiave

  1. Identifica un pattern ripetuto manualmente da più team.
  2. Scrivi la Rule che genera il codice corretto in un solo comando.
  3. Documenta lo schematic in Storybook accanto al componente che scaffolda.

FAQ: Vale la pena uno schematic per un solo componente? Solo se quel componente richiede boilerplate ricorrente (form field, wrapper di validazione); per componenti semplici non serve.

Mini-Guida 3: Configurare Storybook con Addon a11y

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

Passaggi Chiave

  1. Attiva l'addon a11y nel file .storybook/main.ts.
  2. Configura la CI perché fallisca su violazioni a11y critiche, non solo su warning.
  3. Rivedi i risultati direttamente nel pannello Storybook durante lo sviluppo, non solo in CI a fine lavoro.

FAQ: L'addon a11y sostituisce un audit manuale? No: cattura violazioni automatizzabili (contrasto, attributi ARIA mancanti), non problemi di usabilità che richiedono test con utenti reali.

Mini-Guida 4: Pubblicare la Libreria su npm con ng-packagr

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

Passaggi Chiave

  1. Verifica che peerDependencies copra tutte le versioni Angular supportate.
  2. Esegui sempre npm publish --dry-run prima del publish reale.
  3. Automatizza il publish in CI, mai manualmente da un ambiente locale non riproducibile.

FAQ: Serve pubblicare ad ogni merge? No: solo quando il versioning semantico calcolato dai commit produce effettivamente una nuova versione (bugfix, feature, breaking change).

Mini-Guida 5: Esportare Design Token in CSS, SCSS e JSON

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

Passaggi Chiave

  1. Definisci i token in un formato neutro (JSON) come unica fonte di verità.
  2. Genera automaticamente CSS custom properties e SCSS variabili dallo stesso file.
  3. Sincronizza i token con lo strumento di design (Figma) tramite export/import automatizzato, non copia manuale.

FAQ: I designer devono modificare direttamente il JSON? Preferibilmente no: lavorano nello strumento di design, e un plugin/script sincronizza i valori nel repository dei token.

Errori Comuni da Evitare

  • Cambiare la change detection strategy di default a Default: propaga rallentamenti su ogni app che consuma il design system.
  • Usare dependencies invece di peerDependencies per Angular: causa duplicazione del framework nel bundle consumer.
  • Un unico barrel file che esporta tutto: elimina il tree-shaking e gonfia il bundle anche quando si usa un solo componente.
  • Nessun addon a11y in Storybook: le violazioni di accessibilità vengono scoperte solo in produzione, quando costano molto di più da correggere.
  • Publish manuale da laptop: introduce inconsistenza tra ambienti e rende impossibile la tracciabilità delle release.
  • Breaking change senza deprecazione preventiva: rompe silenziosamente ogni app consumer al prossimo aggiornamento.
  • Design token duplicati o hardcoded nei componenti: rende il theming impossibile da mantenere e disallinea design e codice nel tempo.
  • Nessuna governance sui nuovi componenti: porta a duplicati e varianti inconsistenti dello stesso pattern nel giro di pochi mesi.

Domande Frequenti in Sintesi

Cos'è un design system in Angular? Una libreria di componenti riutilizzabili, accessibili e themabili, pubblicata come pacchetto npm, che garantisce coerenza visiva e comportamentale su tutte le applicazioni di un'organizzazione.

Meglio monorepo o repo separato per un design system? Monorepo se il design system serve 1-2 team con iterazione rapida; repo separato quando i team consumer sono 3 o più e serve un'API pubblica stabile e versionata.

Come si pubblica una libreria Angular su npm? Con ng-packagr per generare l'output compilato, peerDependencies per Angular/RxJS, e npm publish automatizzato in CI dopo build e test.

A cosa serve Storybook in un design system? È l'ambiente di sviluppo, documentazione e testing visivo isolato dei componenti, con addon per controlli interattivi e verifica automatica dell'accessibilità.

Come si gestisce il theming di un design system? Tramite design token esportati come CSS custom properties, consumate dai componenti invece di valori hardcoded, così un tema custom si applica sovrascrivendo le variabili a livello di root.

Cosa sono gli schematics Angular? Generatori di codice custom che scaffoldano automaticamente l'uso corretto di un componente o pattern, riducendo l'attrito di adozione rispetto a copiare esempi dalla documentazione.

FAQ

Quanti componenti servono per lanciare la prima versione?

5-8 componenti core coprono la maggior parte dei casi d'uso iniziali e permettono di validare l'intera pipeline prima di scalare.

Serve Nx per costruire un design system Angular?

No, è utile soprattutto in un monorepo con più progetti, ma un design system in repo separato funziona bene anche con il solo Angular CLI.

Come si gestiscono le breaking change?

Con una policy di deprecazione: warning a runtime almeno una minor version prima della rimozione effettiva, documentato nel CHANGELOG.

Il visual testing è obbligatorio?

Fortemente consigliato oltre una decina di componenti condivisi: senza di esso le regressioni visive vengono scoperte solo dagli utenti finali.

Come si misura il successo di un design system?

Con KPI oggettivi: riduzione del tempo di sviluppo per schermata, riduzione dei bug di UI in produzione, tempo di onboarding dei nuovi sviluppatori.

Serve TypeScript strict mode per una libreria pubblica?

Sì, è fortemente raccomandato: una libreria consumata da più team beneficia più di ogni altro codice da un sistema di tipi rigoroso.

Come si testano i componenti con stato complesso?

Con unit test mirati sulla logica interna più visual testing per l'output renderizzato, riservando l'E2E solo ai flussi di interazione più complessi.

I design token vanno versionati insieme ai componenti?

Sì, nello stesso repository e nello stesso ciclo di release, perché un cambio di token è a tutti gli effetti un cambio dell'API visiva.

Come si evita che ogni team crei varianti divergenti dello stesso componente?

Con un processo di contribuzione chiaro che richiede di verificare l'esistenza di un pattern simile prima di crearne uno nuovo.

Quanto tempo richiede costruire un design system maturo?

Tipicamente 3-6 mesi per una libreria solida con 20+ componenti, governance e pipeline CI/CD completa, a seconda delle dimensioni del team dedicato.

Conclusione

Un design system Angular ben costruito non è un progetto "una tantum": è un prodotto interno con i suoi utenti (gli sviluppatori dei team consumer), la sua roadmap e la sua governance. Componenti OnPush con API tipizzata, Storybook come documentazione vivente, ng-packagr per un packaging corretto e una pipeline CI/CD che automatizza test, visual regression e publish sono gli elementi che distinguono un design system realmente adottato da una libreria di componenti abbandonata dopo pochi mesi.

Vuoi una checklist stampabile o una valutazione della tua libreria di componenti esistente? Richiedi un audit tecnico: in poche ore di analisi è possibile identificare i gap di accessibilità, performance e governance prioritari per il tuo design system.

💬 Note dei lettori

0 note

Scrivi una nota

Condividi la tua opinione, un suggerimento o un complimento

Ultime note

Nessuna nota ancora. Sii il primo a commentare!