Nos bureaux

  • Exceev Consulting
    61 Rue de Lyon
    75012, Paris, France
  • Exceev Technology
    332 Bd Brahim Roudani
    20330, Casablanca, Maroc

Suivez-nous

Préférences

Kit de marque

11 min de lecture - Construire un design system Angular : les leçons de SoarUI

Design Systems & Angular

Un design system fait partie de ces investissements qui semblent coûteux au départ et indispensables six mois plus tard. Lorsque nous avons commencé à construire SoarUI, notre design system Angular interne, nous avons pris des décisions qui nous ont fait gagner des centaines d’heures — et commis des erreurs qui nous en ont coûté des semaines. Cet article rassemble les véritables leçons tirées du développement d’un design system utilisé par nos produits et nos équipes.

Que vous démarriez un design system de zéro ou que vous refactorisiez une bibliothèque de composants existante, ces bonnes pratiques et ces écueils s’appliquent à toute boîte à outils d’interface utilisateur basée sur Angular.

Ce que vous allez apprendre

  • Pourquoi un design system dédié se rentabilise en matière de cohérence, de vélocité et d’accessibilité
  • Comment structurer un monorepo pour un design system Angular multi-packages
  • Le theming à base de tokens avec les CSS custom properties et un pipeline de design tokens
  • Les principes de conception d’API de composants qui rendent les équipes consommatrices productives
  • Comment intégrer l’accessibilité dès le premier jour plutôt que de l’ajouter après coup
  • Les stratégies de documentation avec Storybook pour Angular
  • La gestion du versioning et des breaking changes avec semver et des schematics de migration

L’essentiel

Un design system Angular réussi repose sur quatre piliers : une couche de theming à base de tokens utilisant les CSS custom properties, un contrat d’API de composants strict reposant sur les inputs Angular et la content projection, une conformité d’accessibilité testée au niveau du composant, et une stratégie de versioning semver avec des schematics de migration automatisées pour les breaking changes.

Pourquoi construire un design system

Avant d’écrire le moindre composant, il faut pouvoir répondre clairement à la question « pourquoi ne pas simplement utiliser Material ? ». Les raisons qui ont justifié SoarUI — et qui justifient la plupart des design systems maison — se répartissent en trois catégories.

Cohérence entre les produits. Lorsque quatre applications partagent une même marque, un package npm partagé garantit qu’un bouton aura partout la même apparence et le même comportement. Copier-coller du code de composant entre dépôts garantit l’inverse.

Vélocité de développement. Un design system bien documenté transforme un ticket de fonctionnalité de « construire une page de paramètres » en « composer une page de paramètres ». Les développeurs cessent de réinventer des mises en page de formulaires et se concentrent sur la logique métier. Sur SoarUI, nous avons mesuré une réduction de 35 % du temps de développement front-end pour les nouveaux écrans fonctionnels une fois le socle de composants stabilisé.

L’accessibilité par défaut. Lorsque l’accessibilité est intégrée au design system, chaque application consommatrice hérite de composants conformes. C’est bien plus fiable que de former chaque équipe produit à ajouter elle-même les attributs ARIA.

Si votre organisation déploie moins de trois surfaces front-end distinctes, une bibliothèque de composants partagée au sein d’un monorepo peut suffire. Un design system complet, avec tokens, documentation et versioning, devient pertinent une fois ce seuil franchi.

Décisions d’architecture : monorepo, CDK et composants standalone

Structure du monorepo

SoarUI vit dans un monorepo Nx avec une séparation claire des packages :

soarui/
├── packages/
│   ├── tokens/            # Design tokens (JSON → CSS/SCSS/TS)
│   ├── core/              # Base styles, mixins, token consumers
│   ├── components/        # Angular component library
│   ├── icons/             # SVG icon system
│   └── schematics/        # ng update migration scripts
├── apps/
│   ├── docs/              # Storybook documentation site
│   └── playground/        # Development sandbox
├── tools/
│   └── token-pipeline/    # Token transformation scripts
└── nx.json

Chaque package est publié indépendamment. Le package tokens n’a aucune dépendance à Angular — il produit uniquement des CSS custom properties et des définitions de types TypeScript. Cela permet aux consommateurs non-Angular (un site marketing en Astro, par exemple) d’utiliser malgré tout la couche de tokens.

Le CDK Angular comme fondation

Plutôt que de reconstruire des comportements primitifs à partir de zéro, SoarUI s’appuie fortement sur le CDK Angular. Les overlays, le focus trapping, la navigation clavier pour les listboxes et le virtual scrolling proviennent tous de @angular/cdk. Le CDK fournit des primitives comportementales bien testées sans imposer de parti pris visuel :

import { CdkMenuModule } from '@angular/cdk/menu'
import { OverlayModule } from '@angular/cdk/overlay'

@Component({
  selector: 'soar-dropdown',
  standalone: true,
  imports: [CdkMenuModule, OverlayModule],
  template: `
    <button [cdkMenuTriggerFor]="menu">{{ label() }}</button>
    <ng-template #menu>
      <div cdkMenu class="soar-dropdown-panel">
        <ng-content />
      </div>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SoarDropdownComponent {
  label = input.required<string>()
}

Cela nous a fait gagner des mois de travail sur le positionnement des overlays, la gestion du focus et les interactions clavier — des problèmes notoirement difficiles à bien résoudre sur l’ensemble des navigateurs.

Des composants standalone dès le départ

SoarUI a été entièrement construit avec des composants standalone. Chaque composant déclare explicitement ses propres imports, ce qui élimine l’indirection des NgModules et rend le tree-shaking plus efficace pour les consommateurs :

@Component({
  selector: 'soar-button',
  standalone: true,
  imports: [SoarSpinnerComponent, SoarIconComponent],
  templateUrl: './button.component.html',
  styleUrl: './button.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SoarButtonComponent {
  variant = input<'primary' | 'secondary' | 'ghost'>('primary')
  size = input<'sm' | 'md' | 'lg'>('md')
  loading = input(false)
  disabled = input(false)
}

Les consommateurs ajoutent exactement les composants dont ils ont besoin à leur tableau d’imports. Pas de barrel modules, pas d’acrobaties de ré-export.

Theming à base de tokens

Le pipeline de design tokens

Les design tokens sont la source unique de vérité pour chaque décision visuelle : couleurs, espacements, typographie, élévation, rayons de bordure et animations. Dans SoarUI, les tokens sont rédigés en JSON puis transformés en plusieurs formats de sortie à l’aide de Style Dictionary :

{
  "color": {
    "primary": {
      "50": { "value": "#eff6ff" },
      "500": { "value": "#3b82f6" },
      "700": { "value": "#1d4ed8" },
      "900": { "value": "#1e3a5f" }
    },
    "neutral": {
      "100": { "value": "#f5f5f5" },
      "800": { "value": "#262626" }
    }
  },
  "spacing": {
    "xs": { "value": "4px" },
    "sm": { "value": "8px" },
    "md": { "value": "16px" },
    "lg": { "value": "24px" },
    "xl": { "value": "32px" }
  }
}

Le pipeline génère :

  • Des CSS custom properties pour le theming au runtime
  • Des variables SCSS pour un usage au build dans les styles des composants
  • Des constantes TypeScript pour un accès programmatique dans la logique des composants
/* Generated output: soar-tokens.css */
:root {
  --soar-color-primary-50: #eff6ff;
  --soar-color-primary-500: #3b82f6;
  --soar-color-primary-700: #1d4ed8;
  --soar-color-neutral-100: #f5f5f5;
  --soar-color-neutral-800: #262626;
  --soar-spacing-xs: 4px;
  --soar-spacing-sm: 8px;
  --soar-spacing-md: 16px;
}

Le theming avec les CSS custom properties

Chaque composant référence les tokens exclusivement via des CSS custom properties. Aucune valeur hexadécimale codée en dur, aucun nombre magique :

.soar-button {
  padding: var(--soar-spacing-sm) var(--soar-spacing-md);
  border-radius: var(--soar-radius-md);
  font-size: var(--soar-font-size-sm);
  transition: background-color var(--soar-motion-duration-fast) var(--soar-motion-easing-standard);

  &--primary {
    background-color: var(--soar-color-primary-500);
    color: var(--soar-color-on-primary);
  }
}

Le changement de thème devient alors une simple question de surcharge des custom properties au niveau d’un scope parent :

[data-theme='dark'] {
  --soar-color-primary-500: #60a5fa;
  --soar-color-on-primary: #1e3a5f;
  --soar-color-neutral-100: #262626;
  --soar-color-neutral-800: #f5f5f5;
}

Aucun changement dans le code des composants. Aucun coût JavaScript au runtime. Juste la cascade CSS fonctionnant comme prévu.

Principes de conception des API de composants

Un design system vit ou meurt par ses API de composants. Si l’API est maladroite, les équipes la contourneront ou l’éviteront complètement. Nous avons développé quatre principes tout au long de la construction de SoarUI.

Principe 1 : préférer les inputs aux objets de configuration. Des signaux input() individuels sont plus faciles à lier, à documenter et à typer qu’un unique objet de configuration :

<!-- Preferred -->
<soar-button variant="secondary" size="lg" [loading]="isSaving()">Save</soar-button>

<!-- Avoid -->
<soar-button [config]="{ variant: 'secondary', size: 'lg', loading: isSaving() }">Save</soar-button>

Principe 2 : utiliser la content projection pour la composition. Les slots via ng-content permettent aux consommateurs de composer librement, sans que le design system n’impose chaque mise en page :

@Component({
  selector: 'soar-card',
  standalone: true,
  template: `
    <div class="soar-card">
      <div class="soar-card__header">
        <ng-content select="[soarCardHeader]" />
      </div>
      <div class="soar-card__body">
        <ng-content />
      </div>
      <div class="soar-card__footer">
        <ng-content select="[soarCardFooter]" />
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SoarCardComponent {}

Les consommateurs l’utilisent naturellement :

<soar-card>
  <h3 soarCardHeader>Invoice #1042</h3>
  <p>Amount: $1,250.00</p>
  <button soarCardFooter soar-button variant="primary">Pay Now</button>
</soar-card>

Principe 3 : émettre des outputs granulaires. Chaque interaction utilisateur significative obtient son propre output() plutôt qu’un bus d’événements générique :

closed = output<void>()
confirmed = output<string>()
dismissed = output<void>()

Principe 4 : des valeurs par défaut sensées, une surcharge totale possible. Chaque input doit avoir une valeur par défaut qui couvre le cas des 80 %. Les 20 % restants peuvent être surchargés explicitement. Cela permet de garder les usages simples simples, et les usages complexes possibles.

L’accessibilité dès le premier jour

L’accessibilité n’a jamais été une phase dans SoarUI — c’était une contrainte dès le premier composant. Ajouter l’accessibilité a posteriori sur un design system existant est un ordre de grandeur plus difficile que de l’intégrer dès le départ.

Les attributs ARIA comme mécanique interne des composants

Les composants gèrent eux-mêmes leurs attributs ARIA. Les consommateurs ne devraient jamais avoir besoin d’ajouter manuellement role, aria-label ou aria-expanded :

@Component({
  selector: 'soar-accordion-item',
  standalone: true,
  template: `
    <button
      class="soar-accordion__trigger"
      [attr.aria-expanded]="isExpanded()"
      [attr.aria-controls]="panelId"
      (click)="toggle()"
    >
      <ng-content select="[soarAccordionHeader]" />
    </button>
    <div
      [id]="panelId"
      role="region"
      [attr.aria-labelledby]="triggerId"
      [class.soar-accordion__panel--expanded]="isExpanded()"
    >
      <ng-content />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SoarAccordionItemComponent {
  isExpanded = signal(false)
  panelId = `soar-accordion-panel-${uniqueId++}`
  triggerId = `soar-accordion-trigger-${uniqueId++}`

  toggle() {
    this.isExpanded.update((v) => !v)
  }
}

Navigation au clavier

Chaque composant interactif prend en charge la navigation clavier complète. Les dropdowns répondent aux touches fléchées, la touche Échap ferme les overlays, l’ordre de tabulation suit le flux logique du document. Le CDK Angular gère l’essentiel de cela, mais les composants personnalisés nécessitent des liaisons @HostListener explicites ou des directives CDK pour les événements clavier.

Tests d’accessibilité automatisés

Nous exécutons des vérifications axe-core au sein des tests au niveau composant, à l’aide de jest-axe :

import { axe, toHaveNoViolations } from 'jest-axe'

expect.extend(toHaveNoViolations)

it('should have no accessibility violations', async () => {
  const { container } = await render(SoarButtonComponent, {
    componentInputs: { variant: 'primary' },
  })
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})

Chaque PR de composant doit passer les vérifications axe avant d’être mergée. Cela permet de détecter la majorité des violations WCAG — labels manquants, contraste insuffisant, rôles incorrects — avant qu’elles n’atteignent la production.

Documentation et intégration de Storybook

Un design system sans documentation n’est qu’une bibliothèque de composants que personne n’utilise. SoarUI utilise Storybook pour Angular comme principale surface de documentation.

Chaque composant possède un fichier story qui illustre chaque variante, chaque état et chaque cas limite :

export default {
  title: 'Components/Button',
  component: SoarButtonComponent,
  argTypes: {
    variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] },
    size: { control: 'select', options: ['sm', 'md', 'lg'] },
    loading: { control: 'boolean' },
    disabled: { control: 'boolean' },
  },
} as Meta<SoarButtonComponent>

export const Primary: StoryObj<SoarButtonComponent> = {
  args: { variant: 'primary', size: 'md' },
}

export const Loading: StoryObj<SoarButtonComponent> = {
  args: { variant: 'primary', loading: true },
}

Au-delà des démos interactives, chaque page de composant inclut :

  • Des recommandations d’usage — quand utiliser ce composant plutôt qu’une alternative
  • Une référence d’API — chaque input, output et slot de content projection
  • Des notes d’accessibilité — interactions clavier et comportement des lecteurs d’écran
  • Des exemples à faire/à ne pas faire — exemples visuels d’usages corrects et incorrects

Le site de documentation se déploie automatiquement à chaque merge sur main, garantissant qu’il reflète toujours la dernière version publiée.

Stratégie de versioning et de gestion des breaking changes

Les design systems posent un défi de versioning particulier : les breaking changes se répercutent sur chaque application consommatrice. Nous avons adopté une stratégie stricte qui équilibre évolution et stabilité.

Un versioning sémantique avec des règles claires

SoarUI suit semver de façon stricte :

  • Patch — corrections de bugs, améliorations d’accessibilité, ajustements de style qui ne modifient pas l’API
  • Minor — nouveaux composants, nouveaux inputs optionnels, nouvelles valeurs de tokens
  • Major — composants supprimés, inputs renommés, changement de comportement par défaut, restructuration des tokens

Les schematics de migration

Pour chaque montée de version major, nous livrons des schematics Angular qui automatisent la migration. Lorsque nous avons renommé color en variant sur le composant bouton en v3, le schematic s’en est chargé :

export function updateButtonVariantInput(): Rule {
  return (tree: Tree) => {
    tree.visit((filePath) => {
      if (!filePath.endsWith('.html')) return
      const content = tree.read(filePath)?.toString()
      if (!content) return

      const updated = content.replace(/\[color\]="([^"]+)"/g, '[variant]="$1"')
      if (updated !== content) {
        tree.overwrite(filePath, updated)
      }
    })
  }
}

Les équipes produit internes exécutent les schémas de mise à jour du package partagé et les changements s’appliquent automatiquement. Cela réduit considérablement la friction des montées de version majeures et permet aux projets consommateurs de rester à jour.

Cycle de vie de la dépréciation

Avant de supprimer quoi que ce soit, nous suivons un cycle de vie en trois étapes :

  1. Déprécier — marquer l’API avec un JSDoc @deprecated et émettre un avertissement en console en mode dev
  2. Migrer — livrer le schematic de migration dans la prochaine version minor
  3. Supprimer — retirer l’API dépréciée dans la prochaine version major

Cela laisse aux équipes consommatrices au moins un cycle complet de version minor pour s’adapter.

Leçons apprises et erreurs à éviter

Au fil de la construction et de la maintenance de SoarUI, voici les leçons que nous aurions aimé connaître dès le premier jour.

Commencez par les tokens, pas par les composants. Nous avons construit les composants d’abord et ajouté les tokens après coup. Cela a provoqué une migration douloureuse où chaque valeur de couleur et d’espacement codée en dur a dû être remplacée. Définissez votre architecture de tokens avant d’écrire votre premier composant.

N’abstrayez pas trop tôt. Notre première version comportait un SoarFormField générique qui tentait de gérer les champs texte, les selects, les textareas et les date pickers via un unique input type. Il est devenu ingérable en quelques mois. Des composants séparés avec des API ciblées valent toujours mieux qu’un composant fourre-tout.

Investissez tôt dans des guidelines de contribution. Lorsque d’autres équipes ont commencé à contribuer des composants, des incohérences se sont glissées — conventions de nommage différentes, patterns de tests différents, structures de story différentes. Un guide CONTRIBUTING complet, avec des templates et des règles de linting, permet d’éviter cela.

Figez vos peer dependencies avec soin. Une plage de peer dependency trop permissive (>=14.0.0) a provoqué des ruptures subtiles lorsque les consommateurs mettaient à jour Angular. Nous spécifions désormais des plages minor précises (^18.0.0 || ^19.0.0) et testons chaque version supportée en CI.

Mesurez l’adoption, pas seulement la couverture. La couverture de tests des composants vous dit que la bibliothèque fonctionne. Les métriques d’adoption — combien d’équipes utilisent quels composants, à quelle fréquence les équipes surchargent les styles — vous disent si la bibliothèque est réellement utile. Nous avons ajouté une télémétrie d’usage anonyme (opt-in) pour comprendre quels composants avaient besoin d’être améliorés.

Conclusion

Construire un design system, c’est s’engager dans le temps long. Les décisions d’architecture prises au cours du premier mois — structure du monorepo, pipeline de tokens, conventions d’API des composants, standards d’accessibilité — se traduisent, des années durant, soit en vélocité, soit en friction.

SoarUI nous a appris que les meilleurs design systems ont un parti pris fort sur la structure et une grande permissivité sur la composition. Ils imposent la cohérence via les tokens et l’accessibilité via l’automatisation, tout en laissant aux équipes consommatrices la liberté de construire des interfaces que les auteurs du design system n’avaient jamais imaginées.

Si vous envisagez de construire un design system pour vos applications Angular, commencez par la couche de tokens, appuyez-vous sur le CDK Angular pour les primitives comportementales, et livrez des schematics de migration à chaque breaking change. Vous vous en remercierez dans quelques mois — tout comme chaque équipe qui dépend de votre bibliothèque.

Curieux de savoir comment nous avons construit SoarUI, ou besoin d’aide pour mettre en place un design system au sein de votre organisation ? Contactez-nous pour en discuter.

Vous développez avec Angular ?

Des bibliothèques de composants aux produits complets, nous avons déployé Angular à grande échelle sur des dizaines de projets.

Autres articles

Piloter un cabinet de conseil avec des outils open source

Comment Exceev pilote son activité avec Twenty CRM, ZeroMail, n8n, Ghost, Cal.com et Postiz. Un playbook opérationnel pour reprendre le contrôle de sa stack métier.

Lire la suite

Auto-héberger notre infrastructure : observabilité, sécurité, déploiement

Comment Exceev auto-héberge son infrastructure avec Grafana, Prometheus, Loki, k6, Coolify, Infisical, Docker, Tailscale, Cloudflared, Beszel et Duplicati. Plongée opérationnelle dans l'observabilité, le déploiement, la sécurité et la résilience.

Lire la suite

Parlez-nous de votre projet

Nos bureaux

  • Exceev Consulting
    61 Rue de Lyon
    75012, Paris, France
  • Exceev Technology
    332 Bd Brahim Roudani
    20330, Casablanca, Maroc