Modern Frontend Engineering
Stratégie avec
Avec
Fluid Typography avec
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">
×
</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 ?