MFormations
Modern Frontend Engineering

Chapitre 9

Chapitre 09 — Design System

> Construire un système de design robuste, scalable et accessible pour une organisation front-end moderne.

Design System — Cours complet

1. Qu'est-ce qu'un Design System ?

Un Design System (DS) est un ensemble cohérent de règles, de composants réutilisables et de principes de conception qui régissent la création d'une interface utilisateur. Il fait le pont entre design et développement.

Pourquoi un Design System ?

  • Cohérence visuelle : tous les produits ont la même apparence et le même comportement
  • Productivité : les développeurs ne réinventent pas les patterns à chaque projet
  • Scalabilité : une équipe peut maintenir des dizaines de produits avec un socle commun
  • Maintenance : un changement de style (couleur primaire, typographie) se propage partout
  • Accessibilité : l'accessibilité est intégrée par défaut dans tous les composants
  • Onboarding : les nouveaux arrivants montent en productivité plus rapidement

Composants d'un Design System

Design System
├── Design Tokens (couleurs, typo, spacing, shadows)
├── Composants de base (Button, Input, Select)
├── Composants composites (Modal, Toast, DatePicker)
├── Patterns de page (Layout, Navigation)
├── Guidelines d'utilisation
├── Documentation (Storybook)
└── Assets (icônes, illustrations)

2. Design Tokens

Les Design Tokens sont les atomes du Design System. Ce sont des variables qui représentent les décisions de design : couleurs, typographie, espacements, ombres, breakpoints.

Couleurs

/* Couleurs primaires */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;

Typographie

--font-family-base: 'Inter', system-ui, -apple-system, sans-serif;
--font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;

--font-size-xs: 0.75rem;   /* 12px */
--font-size-sm: 0.875rem;  /* 14px */
--font-size-base: 1rem;    /* 16px */
--font-size-lg: 1.125rem;  /* 18px */
--font-size-xl: 1.25rem;   /* 20px */
--font-size-2xl: 1.5rem;   /* 24px */
--font-size-3xl: 1.875rem; /* 30px */
--font-size-4xl: 2.25rem;  /* 36px */

--line-height-tight: 1.25;
--line-height-base: 1.5;
--line-height-relaxed: 1.75;

Spacing (système 4/8)

--spacing-1: 0.25rem;  /* 4px */
--spacing-2: 0.5rem;   /* 8px */
--spacing-3: 0.75rem;  /* 12px */
--spacing-4: 1rem;     /* 16px */
--spacing-5: 1.25rem;  /* 20px */
--spacing-6: 1.5rem;   /* 24px */
--spacing-8: 2rem;     /* 32px */
--spacing-10: 2.5rem;  /* 40px */
--spacing-12: 3rem;    /* 48px */
--spacing-16: 4rem;    /* 64px */

Shadows

--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);

Breakpoints

--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;

Implémentation JSON (pour multi-plateforme)

{
  "color": {
    "primary": {
      "50": "#eff6ff",
      "500": "#3b82f6",
      "900": "#1e3a8a"
    }
  },
  "font": {
    "size": {
      "base": { "value": "1rem" },
      "lg": { "value": "1.125rem" }
    }
  },
  "spacing": {
    "4": { "value": "1rem" },
    "8": { "value": "2rem" }
  }
}

Les tokens JSON peuvent être transformés en CSS, SCSS, JS/TS, Swift, Kotlin via des outils comme Style Dictionary ou Theo.


3. Dark Mode

Stratégie avec prefers-color-scheme

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-border: #e5e7eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1a1a1a;
    --color-text: #f3f4f6;
    --color-border: #374151;
  }
}

Avec light-dark() (nouvelle API CSS)

:root {
  color-scheme: light dark;
  --color-bg: light-dark(#ffffff, #1a1a1a);
  --color-text: light-dark(#1a1a1a, #f3f4f6);
}

Stratégie avec toggle manuel

[data-theme="dark"] {
  --color-bg: #1a1a1a;
  --color-text: #f3f4f6;
}
const theme = localStorage.getItem('theme');
document.documentElement.setAttribute('data-theme', theme);

document.getElementById('theme-toggle').addEventListener('click', () => {
  const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  document.documentElement.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
});

4. Typographie

Fluid Typography avec clamp()

h1 {
  font-size: clamp(2rem, 5vw + 1rem, 4rem);
}

p {
  font-size: clamp(1rem, 2vw + 0.5rem, 1.25rem);
}

Web Fonts (optimisées)

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
/>
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Variable.woff2') format('woff2');
  font-display: swap;
  font-weight: 100 900;
}

5. Composants

Button

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'ghost' | 'danger';
  size: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  loading?: boolean;
  children: React.ReactNode;
}

function Button({ variant = 'primary', size = 'md', children, ...props }: ButtonProps) {
  return (
    <button className={`btn btn--${variant} btn--${size}`} {...props}>
      {children}
    </button>
  );
}

Input

interface InputProps {
  label: string;
  error?: string;
  hint?: string;
}

function Input({ label, error, hint, ...props }: InputProps & React.InputHTMLAttributes<HTMLInputElement>) {
  return (
    <div className="input-group">
      <label className="input-label">{label}</label>
      <input className={`input ${error ? 'input--error' : ''}`} {...props} />
      {error && <span className="input-error" role="alert">{error}</span>}
      {hint && !error && <span className="input-hint">{hint}</span>}
    </div>
  );
}

Modal

import { useEffect, useRef } from 'react';

function Modal({ isOpen, onClose, title, children }: ModalProps) {
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;
    if (isOpen) dialog.showModal();
    else dialog.close();
  }, [isOpen]);

  return (
    <dialog ref={dialogRef} className="modal" onClose={onClose}>
      <div className="modal-header">
        <h2 className="modal-title">{title}</h2>
        <button className="modal-close" onClick={onClose} aria-label="Fermer">
          &times;
        </button>
      </div>
      <div className="modal-body">{children}</div>
    </dialog>
  );
}

6. Storybook

Configuration

// .storybook/main.ts
export default {
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@storybook/addon-designs',
  ],
  framework: '@storybook/react-vite',
};

Exemple de story

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Primitives/Button',
  component: Button,
  argTypes: {
    variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger'] },
    size: { control: 'select', options: ['sm', 'md', 'lg'] },
    disabled: { control: 'boolean' },
  },
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: { variant: 'primary', children: 'Click me' },
};

export const Loading: Story = {
  args: { variant: 'primary', loading: true, children: 'Saving...' },
};

7. Accessibilité dans le DS

Principes clés

/* Focus visible */
:focus-visible {
  outline: 2px solid var(--color-primary-500);
  outline-offset: 2px;
}

/* État focus pour les utilisateurs clavier uniquement */
.button:focus-visible {
  box-shadow: 0 0 0 3px var(--color-primary-200);
}

/* Contraste minimum */
--color-text: #1a1a1a; /* ratio 13:1 sur fond blanc */
--color-text-muted: #6b7280; /* ratio 4.5:1 minimum */

Rôles ARIA

function Toast({ message, type }: ToastProps) {
  return (
    <div role="alert" aria-live="assertive" className={`toast toast--${type}`}>
      {message}
    </div>
  );
}

8. Tests visuels

Chromatic (intégration Storybook)

# .github/workflows/chromatic.yml
name: Chromatic
on: push
jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: npm ci
      - uses: chromaui/action@v1
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Percy

// percy.config.js
export default {
  version: 2,
  snapshot: {
    widths: [375, 1280],
    minHeight: 1024,
  },
};

9. Versioning et Breaking Changes

Utilisez le Semantic Versioning (SemVer) :

MAJOR.MINOR.PATCH
  • MAJOR : breaking change (suppression de composant, changement d'API)
  • MINOR : nouvelle fonctionnalité rétrocompatible
  • PATCH : correction de bug

Stratégie de migration

# CHANGELOG

## [3.0.0] - 2026-06-15

### Breaking Changes
- Suppression du composant `OldButton`, utiliser `Button` à la place
- Renommage de `--color-primary` en `--color-brand-primary`

### Migration Guide
```css
/* Avant */
color: var(--color-primary);

/* Après */
color: var(--color-brand-primary);

---

## 10. Comparaison des solutions

| Critère | Material UI | Radix UI | shadcn/ui | Headless UI |
|---------|-------------|----------|-----------|-------------|
| **Style** | Inclus (MUI Theme) | Aucun (BYOC) | Tailwind | Aucun |
| **Accessibilité** | Bonne | Excellente | Basée sur Radix | Excellente |
| **Personnalisation** | Limitée | Totale | Totale | Totale |
| **Bundle size** | Lourd (~100kB) | Léger (~5kB/composant) | Léger | Léger |
| **Headless** | Non | Oui | Oui (Radix) | Oui |
| **Rendu SSR** | Oui | Oui | Oui | Oui |
| **Version** | MUI v6+ | Radix v3+ | CLI-based | Tailwind Labs |

---

## 11. Documentation

Documentez chaque composant avec :

1. **Description** : à quoi sert le composant ?
2. **Props** : tableau complet des propriétés
3. **Variants** : exemples visuels de chaque état
4. **Usage guidelines** : quand utiliser ce composant, quand éviter
5. **Accessibilité** : comportement ARIA, navigation clavier
6. **Exemples de code** : extraits copiables
7. **Design guidelines** : espacements, typographie, couleurs

---

## 12. Bonnes pratiques

1. **Commencer petit** : tokens, puis composants atomiques
2. **Itérer** : le DS évolue avec les besoins
3. **Dogfooding** : l'équipe doit utiliser son propre DS
4. **Tests** : unitaires + visuels + accessibilité
5. **Documentation** : toujours à jour
6. **Contributions** : processus clair pour proposer des changements
7. **Gouvernance** : qui décide des ajouts/modifications ?