Design System, Accessibilité & Bibliothèque de Composants

Semaine 6 — Projet de synthèse

Construire une bibliothèque testée, accessible et documentée

Design tokens Accessibilité Tests Documentation

Objectifs de la leçon

1. Design system thinking

Tokens, variantes, composition — une API cohérente pour les développeurs

2. Accessibilité web (a11y)

ARIA, navigation clavier, contraste, screen readers

3. Production-ready

Savoir quand un composant est prêt : props typées, tests, a11y, documentation

4. Combiner les patterns

Assembler testing + performance + patterns avancés dans un projet concret

Plan du cours

1. Recap semaine 6

Testing, performance, patterns avancés

2. Design system thinking

Cohérence, tokens, variantes, slots

3. Accessibilité (a11y)

ARIA, navigation clavier, contraste, screen readers

4. Checklist de qualité

Un composant est prêt quand…

5. Mini-projet : bibliothèque de composants

Démo complète d'une lib testée, accessible et documentée

Module 1

Recap semaine 6

Testing · Performance · Patterns avancés

Recap : Testing (Vitest + RTL)

Vitest

Runner rapide, compatible Jest API, hot module reload

React Testing Library

Tester le comportement, pas l'implémentation — queries par rôle

MSW

Mock Service Worker — intercepter les requêtes réseau sans modifier le code

Principe clé : tester comme un utilisateur interagit — pas comme un développeur lit le code

Recap : Performance

React.memo

Éviter les re-renders si les props n'ont pas changé

useMemo

Mémoriser un calcul coûteux entre les renders

useCallback

Stabiliser une référence de fonction pour les enfants memoized

React.lazy + Suspense

Code-splitting : charger les composants à la demande

Règle d'or : ne jamais optimiser prématurément — mesurer d'abord avec React DevTools Profiler

Recap : Patterns avancés

Custom hooks

Extraire la logique réutilisable dans une fonction use…

Compound components

API déclarative avec Context interne (Radix, shadcn/ui)

Composants headless

Logique + a11y sans imposer de style

Aujourd'hui : on combine TOUT pour construire une bibliothèque de composants robuste

Module 2

Design System Thinking

Cohérence · Tokens · Variantes · Composition

Qu'est-ce qu'un design system ?

Un design system n'est PAS juste du CSS

C'est une API cohérente pour les développeurs — un contrat partagé entre design et code

Ce que c'est

  • Des composants réutilisables avec une API claire
  • Des tokens pour garantir la cohérence visuelle
  • Des variantes prédictibles (size, color, intent)
  • De la documentation et des exemples

Ce que ce n'est pas

  • Un fichier CSS global géant
  • Juste une palette de couleurs dans Figma
  • Des composants copiés-collés sans structure
  • Un one-shot qu'on ne maintient pas

Design tokens : le vocabulaire commun

Les tokens définissent les valeurs de base — couleurs, spacing, typographie, border-radius

Exemples de tokens

color.primary
color.danger
spacing.md = 16px
radius.lg = 12px

Pourquoi des tokens ?

  • 1. Un seul endroit à modifier pour changer le thème
  • 2. Cohérence garantie entre tous les composants
  • 3. Pas de valeurs magiques (#3b82f6) éparpillées
  • 4. Support dark/light mode natif

Tokens en TypeScript

Définir les tokens comme un objet typé — autocomplétion garantie

// tokens.ts — source de vérité

export const tokens = {

colors: {

primary: '#6366f1', // indigo-500

danger: '#ef4444', // red-500

success: '#10b981', // emerald-500

neutral: '#64748b', // slate-500

},

spacing: {

xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px',

},

radius: {

sm: '4px', md: '8px', lg: '12px', full: '9999px',

},

fontSize: {

sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.25rem',

},

} as const

as const rend toutes les valeurs littérales — TypeScript les considère comme des types exacts, pas des string

Variantes : l'API de vos composants

Chaque composant expose des variantes qui contrôlent son apparence et son comportement

// Button.tsx — props avec variantes

interface ButtonProps {

variant: 'primary' | 'secondary' | 'danger' | 'ghost'

size: 'sm' | 'md' | 'lg'

disabled?: boolean

loading?: boolean

children: React.ReactNode

}

Primary
Secondary
Danger
Ghost

Les variantes forment un contrat explicite — l'utilisateur du composant sait exactement quelles combinaisons sont possibles

Implémentation : Button avec variantes

// Map de styles par variante

const variantStyles: Record<ButtonProps['variant'], string> = {

primary: 'bg-indigo-600 hover:bg-indigo-700 text-white',

secondary: 'bg-slate-700 hover:bg-slate-600 text-white',

danger: 'bg-red-600 hover:bg-red-700 text-white',

ghost: 'border border-slate-600 hover:bg-slate-800 text-slate-300',

}

const sizeStyles: Record<ButtonProps['size'], string> = {

sm: 'px-3 py-1.5 text-sm',

md: 'px-4 py-2 text-base',

lg: 'px-6 py-3 text-lg',

}

export function Button({ variant, size, disabled, loading, children }: ButtonProps) {

return (

<button

className={`rounded-lg font-semibold transition ${variantStyles[variant]} ${sizeStyles[size]}`}

disabled={disabled || loading}

aria-busy={loading}

>

{loading ? 'Chargement…' : children}

</button>

)

}

Composition vs Configuration

Préférer les composants composables aux composants ultra-configurables

✗ ERREUR — sur-configuration

<Card

title="Mon titre"

subtitle="Sous-titre"

icon="star"

footer={<Button>OK</Button>}

headerAction={<IconButton />}

borderColor="indigo"

showDivider={true}

/>

// 15 props → impossible à maintenir

✓ CORRECT — composition

<Card>

<Card.Header>

<Card.Title>Mon titre</Card.Title>

<IconButton />

</Card.Header>

<Card.Body>Contenu</Card.Body>

<Card.Footer>

<Button>OK</Button>

</Card.Footer>

</Card>

// Flexible, lisible, extensible

Composant trop spécifique vs générique

✗ ERREUR — trop spécifique

// Un composant pour UN seul cas d'usage

function UserProfileCard() {

return (

<div className="bg-white p-4">

<img src={user.avatar} />

<h3>{user.name}</h3>

<p>{user.email}</p>

</div>

)

}

// Inutilisable ailleurs

✓ CORRECT — composable

// Composants génériques réutilisables

<Card>

<Avatar src={user.avatar} size="md" />

<Text variant="heading">

{user.name}

</Text>

<Text variant="body" color="muted">

{user.email}

</Text>

</Card>

// Chaque pièce est réutilisable

Module 3

Accessibilité (a11y)

ARIA · Navigation clavier · Contraste · Screen readers

Pourquoi l'accessibilité ?

15%

de la population mondiale vit avec un handicap

100%

des utilisateurs bénéficient d'une bonne a11y (clavier, mobile, etc.)

Loi

RGAA en France, ADA aux USA — obligation légale

L'a11y n'est pas optionnelle — c'est un critère de qualité

  • Un composant inaccessible n'est pas production-ready
  • Bonne a11y = meilleur SEO, meilleure UX mobile, meilleure DX testing
  • Les queries getByRole de RTL vérifient implicitement les rôles ARIA

Les rôles ARIA essentiels

ARIA = Accessible Rich Internet Applications — des attributs pour enrichir la sémantique HTML

Attributs de rôle

role="button" — élément cliquable

role="dialog" — fenêtre modale

role="alert" — message important

role="tab" / "tabpanel" — onglets

Attributs d'état

aria-expanded="true" — ouvert/fermé

aria-hidden="true" — masqué au SR

aria-label="Fermer" — label invisible

aria-disabled="true" — désactivé

Règle d'or : préférer le HTML sémantique natif (<button>, <nav>, <dialog>) — ARIA est un correctif quand le HTML ne suffit pas

Navigation clavier

Chaque composant interactif DOIT être utilisable au clavier

Touches attendues

Tab Navigation entre éléments focusables
Enter Activer un bouton / lien
Space Activer un bouton / checkbox
Escape Fermer un modal / dropdown
Arrow Naviguer dans tabs, menus, listes

Erreurs fréquentes

  • <div onClick> — pas focusable, pas de rôle
  • Outline supprimé : outline: none
  • tabIndex="-1" sur un élément interactif
  • Pas de focus trap dans les modals
  • Pas de gestion d'Escape pour fermer

Exemple : Modal accessible

Focus trap + Escape + aria-modal + retour du focus

function Modal({ isOpen, onClose, title, children }: ModalProps) {

const previousFocus = useRef<HTMLElement | null>(null)

useEffect(() => {

if (isOpen) {

previousFocus.current = document.activeElement as HTMLElement

// Focus le premier élément focusable dans le modal

}

return () => previousFocus.current?.focus() // Retour du focus

}, [isOpen])

if (!isOpen) return null

return (

<div role="dialog" aria-modal="true" aria-labelledby="modal-title"

onKeyDown={e => e.key === 'Escape' && onClose()}>

<h2 id="modal-title">{title}</h2>

{children}

<button onClick={onClose} aria-label="Fermer">✕</button>

</div>

)

}

Contraste et couleurs — WCAG

Le ratio de contraste minimum entre le texte et le fond

WCAG AA (minimum)

4.5:1 Texte normal
3:1 Texte large (18px+ ou 14px+ bold)

WCAG AAA (idéal)

7:1 Texte normal
4.5:1 Texte large

Blanc sur indigo

Ratio ≈ 4.6:1 ✓

Blanc sur jaune

Ratio ≈ 1.1:1 ✗

Gris clair sur dark

Ratio ≈ 11:1 ✓

Screen readers : comment ils lisent votre UI

Un lecteur d'écran parcourt le DOM sémantique — pas le visuel

🔊

Ce que le SR annonce :

"Bouton, Supprimer l'élément" — rôle + label accessible

👁

Ce que l'utilisateur voit :

Un bouton rouge avec une icône poubelle

HTML sémantique = gratuit

  • <button> → rôle "button" automatique
  • <nav> → rôle "navigation"
  • <h1>–<h6> → structure de titres
  • <input type="email"> → champ email

Divs = silence

  • <div onClick> → rien annoncé
  • <span class="title"> → pas un titre
  • <div class="nav"> → pas une nav
  • <img> sans alt → image ignorée

Tester l'accessibilité avec RTL

Les queries getByRole vérifient implicitement les rôles ARIA

// Si getByRole trouve l'élément, c'est qu'il est accessible !

import { render, screen } from '@testing-library/react'

it('le bouton est accessible', () => {

render(<Button variant="primary" size="md">Sauvegarder</Button>)

// ✓ Vérifie le rôle "button" ET le texte accessible

const btn = screen.getByRole('button', { name: 'Sauvegarder' })

expect(btn).toBeInTheDocument()

})

it('le modal a le bon rôle', () => {

render(<Modal isOpen title="Confirmer" onClose={vi.fn()}>...</Modal>)

// ✓ Vérifie role="dialog" ET aria-labelledby

const dialog = screen.getByRole('dialog', { name: 'Confirmer' })

expect(dialog).toBeInTheDocument()

})

Outil : axe-core pour les tests automatisés

Détecter les violations d'accessibilité automatiquement dans vos tests

// setup : npm install -D vitest-axe

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

expect.extend(toHaveNoViolations)

it('le composant n\'a aucune violation a11y', async () => {

const { container } = render(<Alert variant="warning">Attention !</Alert>)

const results = await axe(container)

expect(results).toHaveNoViolations()

})

axe-core teste : contraste, labels manquants, rôles incorrects, structure des titres, focus order…

Piège classique : div clickable vs button

✗ ERREUR — div avec onClick

<div

className="cursor-pointer bg-blue-500"

onClick={handleDelete}

>

Supprimer

</div>

✗ Pas focusable au clavier

✗ Pas de rôle "button"

✗ Enter/Space ne marche pas

✗ Screen reader l'ignore

✓ CORRECT — button natif

<button

className="bg-blue-500 rounded px-4 py-2"

onClick={handleDelete}

aria-label="Supprimer l'élément"

>

Supprimer

</button>

✓ Focusable nativement

✓ Rôle "button" automatique

✓ Enter + Space activent le click

✓ Screen reader annonce le bouton

Module 4

Checklist de qualité

Un composant est prêt quand…

Un composant est production-ready quand…

1. Props typées

Interface TypeScript, valeurs par défaut, documentation JSDoc

2. Tests

Comportement, variantes, cas limites, a11y (axe-core)

3. Accessible

Navigable au clavier, rôles ARIA, contraste suffisant

4. Tous les états

hover, focus, disabled, loading, error, empty

5. Documenté

Page démo style Storybook avec exemples de chaque variante

6. Performant

Pas de re-render inutile, memo si nécessaire

Si UN seul critère manque, le composant n'est pas prêt pour la production

Props typées : l'API de votre composant

// Alert.tsx — typage strict avec documentation

interface AlertProps {

/** Variante visuelle de l'alerte */

variant: 'info' | 'success' | 'warning' | 'error'

/** Titre optionnel affiché en gras */

title?: string

/** Contenu principal */

children: React.ReactNode

/** Si true, affiche un bouton pour fermer */

dismissible?: boolean

/** Callback quand l'utilisateur ferme l'alerte */

onDismiss?: () => void

}

Bonnes pratiques : JSDoc sur chaque prop, union types pour les variantes, optional pour les props facultatives, valeurs par défaut via destructuring

Que tester dans un composant ?

À tester

  • Le rendu par défaut (snapshot minimal)
  • Chaque variante a le bon style/rôle
  • Les interactions (click, clavier)
  • Les cas limites (texte vide, très long)
  • L'état disabled ne trigger pas les actions
  • Accessibilité (axe-core, rôles)

À NE PAS tester

  • Les détails d'implémentation CSS
  • Les class names exactes
  • La structure DOM interne
  • Les valeurs de state internes

Tester le COMPORTEMENT visible par l'utilisateur — pas l'implémentation

Gérer TOUS les états d'un composant

Un composant sans ses états est un composant incomplet

Sauvegarder

Default

Sauvegarder

Hover / Focus

Sauvegarder

Disabled

Chargement…

Loading

Erreur !

Error

Sauvegardé ✓

Success

Piège : oublier les états loading et error — l'utilisateur ne sait pas ce qui se passe

Module 5

Mini-projet : Bibliothèque de composants

Démo complète d'une lib testée, accessible et documentée

Structure du projet

// Architecture recommandée

src/

components/

Button/

Button.tsx ← composant

Button.test.tsx ← tests

Button.stories.tsx ← démo / docs

index.ts ← export

Alert/

Alert.tsx

Alert.test.tsx

Alert.stories.tsx

index.ts

Modal/

...

tokens/

tokens.ts ← design tokens

index.ts ← barrel export

Démo : composant Alert complet

Tokens + variantes + accessibilité + tous les états

// Alert.tsx

const variantConfig = {

info: { bg: 'bg-blue-900/40', border: 'border-blue-500', icon: 'ℹ️', role: 'status' },

success: { bg: 'bg-emerald-900/40', border: 'border-emerald-500', icon: '✓', role: 'status' },

warning: { bg: 'bg-yellow-900/40', border: 'border-yellow-500', icon: '⚠', role: 'alert' },

error: { bg: 'bg-red-900/40', border: 'border-red-500', icon: '✗', role: 'alert' },

} as const

export function Alert({ variant, title, children, dismissible, onDismiss }: AlertProps) {

const config = variantConfig[variant]

return (

<div

role={config.role}

className={`p-4 rounded-lg border-l-4 ${config.bg} ${config.border}`}

>

<span aria-hidden="true">{config.icon}</span>

{title && <strong>{title}</strong>}

<p>{children}</p>

{dismissible && (

<button onClick={onDismiss} aria-label="Fermer l'alerte">✕</button>

)}

</div>

)

}

Tests du composant Alert

// Alert.test.tsx

import { render, screen } from '@testing-library/react'

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

expect.extend(toHaveNoViolations)

describe('Alert', () => {

it('affiche le contenu et le titre', () => {

render(<Alert variant="info" title="Info">Message</Alert>)

expect(screen.getByText('Info')).toBeInTheDocument()

expect(screen.getByText('Message')).toBeInTheDocument()

})

it('error/warning ont role="alert"', () => {

render(<Alert variant="error">Erreur !</Alert>)

expect(screen.getByRole('alert')).toBeInTheDocument()

})

it('le bouton dismiss appelle onDismiss', async () => {

const onDismiss = vi.fn()

render(<Alert variant="info" dismissible onDismiss={onDismiss}>X</Alert>)

await userEvent.click(screen.getByRole('button', { name: "Fermer l'alerte" }))

expect(onDismiss).toHaveBeenCalledOnce()

})

it('n\'a aucune violation a11y', async () => {

const { container } = render(<Alert variant="warning">Attention</Alert>)

expect(await axe(container)).toHaveNoViolations()

})

})

Page de documentation (Storybook-style)

Chaque composant a une page démo avec toutes ses variantes et états

// Alert.stories.tsx — démo interactive

export default { title: 'Components/Alert', component: Alert }

export const Info = () => (

<Alert variant="info" title="Information">

Votre profil a été mis à jour.

</Alert>

)

export const Error = () => (

<Alert variant="error" title="Erreur" dismissible>

Impossible de sauvegarder. Veuillez réessayer.

</Alert>

)

export const AllVariants = () => (

<div className="space-y-4">

{['info', 'success', 'warning', 'error'].map(v => (

<Alert key={v} variant={v} title={v}>Exemple {v}</Alert>

))}

</div>

)

Pièges courants — vue d'ensemble

1. Style sans comportement

Se concentrer sur l'apparence et oublier le clavier, les screen readers, et les états loading/error

2. Composants trop spécifiques

Créer UserProfileCard au lieu de Card + Avatar + Text composables

3. Pas de cas limites

Texte très long, liste vide, props manquantes, contenu dynamique — jamais testés

4. États manquants

hover, focus, disabled, loading, error, empty — au moins un oublié dans chaque composant non vérifié

Piège 1 — Style sans comportement

✗ ERREUR — beau mais inaccessible

function Dropdown({ items }) {

const [open, setOpen] = useState(false)

return (

<div onClick={() => setOpen(!open)}>

Choisir ▼

{open && items.map(...)}

</div>

)

}

// Pas de focus, pas de clavier, pas d'ARIA

✓ CORRECT — comportement complet

function Dropdown({ items, label }) {

return (

<button

aria-expanded={open}

aria-haspopup="listbox"

onKeyDown={handleKeyDown}

>

{label} ▼

</button>

<ul role="listbox">...</ul>

)

}

// Clavier + ARIA + focus management

Piège 3 — Oublier les cas limites

✗ ERREUR — tests uniquement "happy path"

it('affiche la liste', () => {

render(<List items={['a', 'b']} />)

expect(screen.getByText('a')).toBeInTheDocument()

})

// Et si items est vide ? null ? très long ?

✓ CORRECT — cas limites couverts

it('affiche "Aucun élément" si vide', () => {

render(<List items={[]} />)

expect(screen.getByText('Aucun élément'))

})

it('tronque les textes très longs', () => {

render(<List items={['a'.repeat(500)]} />)

// Vérifier que le layout ne casse pas

})

Le mini-projet : consignes

Construire une bibliothèque de 3-5 composants :

Composants suggérés

  • • Button (variantes, sizes, loading)
  • • Alert (info, success, warning, error)
  • • Modal (focus trap, Escape, a11y)
  • • TextField (label, error, helper text)
  • • Badge (variantes, dot, count)

Critères d'évaluation

  • • ✓ Props typées avec TypeScript
  • • ✓ Tests unitaires (Vitest + RTL)
  • • ✓ Accessibilité (clavier + ARIA + axe)
  • • ✓ Tokens partagés (couleurs, spacing)
  • • ✓ Page démo documentée

Workflow recommandé

1️⃣

Définir les tokens

Couleurs, spacing, radius, typography — tokens.ts

2️⃣

Définir l'interface (props)

Quelles variantes ? Quels états ? Quelle API ?

3️⃣

Écrire les tests en premier (TDD)

Tester le comportement attendu AVANT d'implémenter

4️⃣

Implémenter le composant

Tokens + variantes + ARIA + clavier — les tests guident

5️⃣

Documenter avec une page démo

Toutes les variantes et états visibles sur une seule page

Points clés à retenir

Design system ≠ CSS

C'est une API cohérente : tokens + variantes + composition = contrat partagé

Chaque composant doit avoir

Props typées, tests, accessibilité clavier, documentation — sinon pas prêt

Tokens = cohérence

Couleurs, spacing, border-radius définis à UN seul endroit — plus de valeurs magiques

getByRole = test d'a11y gratuit

Si la query trouve l'élément, c'est que son rôle ARIA est correct — vérifié implicitement

La semaine en un mot : QUALITÉ

Un composant production-ready combine : TypeScript strict + tests complets + a11y native + tous les états gérés + documentation claire