Tutoriels

Construire un système de couleurs pour votre design system

13 min de lecture

Les choix de couleurs pris au fil de l'eau s'accumulent en chaos. Un bleu de marque spécifié comme #3B82F6 dans un composant, #2563EB dans un autre et rgba(59, 130, 246, 0.8) dans un troisième — tous censés être le « même » bleu — crée une incohérence visuelle qui sape la cohésion de la marque, rend le mode sombre impossible à implémenter de façon fiable et transforme chaque changement de couleur en une fouille archéologique du codebase.

Un système de couleurs résout ce problème en établissant une source de vérité unique pour chaque couleur de votre interface, organisée en deux couches : une palette de tokens primitifs regroupant toutes les valeurs de couleur disponibles, et une couche de tokens sémantiques qui associe ces primitifs à leurs rôles prévus. Cette séparation permet de construire le mode sombre, le mode à fort contraste et les variantes de marque en modifiant les assignations de tokens sémantiques — sans toucher au moindre code de composant.

Ce guide explique comment construire de bout en bout un système de couleurs complet et prêt pour la production : choisir une palette de primitifs, générer des échelles de nuances, définir des tokens sémantiques, les implémenter en CSS et Tailwind, et mettre en place la documentation et les tests.


Pourquoi les couleurs systématiques sont importantes

L'argument en faveur de couleurs systématiques n'est pas esthétique — il est opérationnel. Voici ce qui se casse sans système :

L'incohérence se cumule avec le temps : en début de projet, les designers et les développeurs prennent des décisions de couleur au cas par cas qui « semblent bonnes ». Au bout de deux ans, un codebase a accumulé 40 valeurs de bleu distinctes qui sont toutes censées être le même bleu. Personne ne sait laquelle est correcte.

Le mode sombre exige un mapping sémantique : le mode sombre ne peut pas fonctionner si les couleurs sont codées en dur par leur valeur visuelle. color: #1E293B est une couleur sombre qui convient bien au texte sur fond clair. En mode sombre, vous avez besoin d'une valeur différente. Si vous avez codé en dur #1E293B dans 80 composants, vous avez 80 endroits à mettre à jour — ou un système de substitution CSS complexe qui crée une nouvelle couche de dette technique. Les tokens sémantiques éliminent ce problème.

Les changements de thème sont coûteux : rebrander ou changer une couleur principale devrait être une modification unique. Avec un système sémantique, vous changez --color-primary-600 dans le fichier de tokens et chaque bouton, lien, anneau de focus et bordure qui le référence se met à jour automatiquement.

Les audits d'accessibilité échouent silencieusement : sans système, vérifier que tout le texte passe les exigences de contraste WCAG nécessite d'auditer manuellement chaque combinaison texte/fond. Avec des tokens sémantiques, vous pouvez définir des règles (« text-primary sur surface-primary doit passer AA ») et les tester de façon programmatique.


Conventions de nommage : primitifs vs sémantiques

Un système de couleurs bien structuré comporte deux niveaux de nommage qui servent des objectifs différents.

Tokens de couleurs primitifs

Les tokens primitifs sont les valeurs de couleur brutes, nommées selon leur position dans une échelle. Ils ne font aucune hypothèse sur l'usage — ce sont simplement une palette de couleurs disponibles :

blue-50   → #EFF6FF
blue-100  → #DBEAFE
blue-200  → #BFDBFE
blue-300  → #93C5FD
blue-400  → #60A5FA
blue-500  → #3B82F6
blue-600  → #2563EB
blue-700  → #1D4ED8
blue-800  → #1E40AF
blue-900  → #1E3A8A
blue-950  → #172554

Les noms de tokens primitifs répondent à la question : « Quelle est cette couleur ? » Ils ne répondent pas à la question : « À quoi sert cette couleur ? »

Tokens de couleurs sémantiques

Les tokens sémantiques référencent les tokens primitifs et nomment les couleurs par leur rôle :

color-action-primary       → blue-600
color-action-primary-hover → blue-700
color-action-primary-text  → white-0

color-surface-default      → neutral-50 (mode clair) / neutral-900 (mode sombre)
color-surface-elevated     → white-0 (mode clair) / neutral-800 (mode sombre)

color-text-primary         → neutral-900 (mode clair) / neutral-50 (mode sombre)
color-text-secondary       → neutral-600 (mode clair) / neutral-400 (mode sombre)
color-text-disabled        → neutral-400 (mode clair) / neutral-600 (mode sombre)

color-border-default       → neutral-200 (mode clair) / neutral-700 (mode sombre)
color-border-focused       → blue-500

Les noms de tokens sémantiques répondent à la question : « À quoi sert cette couleur ? » Ils ne spécifient pas de valeur — ils référencent un primitif.

Cette séparation est la décision structurelle clé. Les composants référencent des tokens sémantiques, jamais des primitifs. Quand vous définissez le mode sombre, vous remappez les tokens sémantiques vers d'autres primitifs — aucune modification du code des composants n'est nécessaire.

Conventions de nommage dans la nature

Différents design systems utilisent différentes conventions de nommage pour leur couche sémantique :

Système Exemple de nom sémantique Convention
Material Design sys.color.primary Hiérarchie de rôles séparée par des points
Apple HIG label, secondaryLabel Noms de rôles simples
Radix UI --accent-9, --gray-11 Noms sémantiques basés sur l'échelle
Primer (GitHub) fgColor.default, bgColor.default Rôle + variante

La convention importe moins que la cohérence. Choisissez-en une et appliquez-la uniformément.


Génération d'échelles de nuances (50–950)

Une échelle de nuances est une série ordonnée de valeurs de couleur du quasi-blanc au quasi-noir au sein d'une même famille de teinte. Tailwind CSS a popularisé l'échelle 50–950 ; Material Design utilise 100–900. Les chiffres précis importent moins que d'avoir suffisamment de paliers pour couvrir tous les cas d'usage : teintes très claires pour les fonds, valeurs intermédiaires pour les éléments interactifs et nuances profondes pour le texte.

Ce qui fait une bonne échelle de nuances

Une échelle de nuances utilisable possède deux propriétés :

  1. Suffisamment de paliers perceptuels : les nuances adjacentes doivent être visuellement distinctes — ni si proches que la différence est imperceptible, ni si éloignées que des lacunes apparaissent.
  2. Progression de luminosité cohérente : les valeurs de luminosité doivent augmenter de façon prévisible de 950 (le plus sombre) à 50 (le plus clair), sans sauts ni inversions inattendus.

Le défaut des échelles de nuances choisies à la main est un espacement de luminosité irrégulier. Une échelle où les nuances 300, 400 et 500 sont toutes perceptuellement similaires et où il y a ensuite un saut dramatique de 500 à 700 n'est pas pratique — vous attrapez toujours le 500 quand vous avez besoin de quelque chose au milieu de la plage.

Générer des nuances en OKLCH

OKLCH (Lightness-Chroma-Hue perceptuellement uniforme) est le meilleur espace colorimétrique pour la génération de nuances, car son canal de luminosité est perceptuellement uniforme — des paliers numériques égaux produisent des paliers perçus égaux. Si vous espacez les valeurs de luminosité de L=0,15 (très sombre) à L=0,98 (très clair) par incréments égaux, les nuances résultantes paraîtront visuellement équidistantes.

Pour générer une échelle de nuances en 11 paliers en OKLCH :

  1. Choisissez votre teinte et votre chroma de base : par ex., H=250 (bleu), C=0,20.
  2. Espacez les valeurs de luminosité uniformément : L = 0,15, 0,22, 0,30, 0,38, 0,47, 0,55, 0,63, 0,72, 0,82, 0,92, 0,97.
  3. Optionnellement, faites varier légèrement le chroma : réduisez le chroma aux extrêmes (très clair et très sombre) où les couleurs saturées semblent déplacées.
  4. Convertissez les valeurs OKLCH en hex pour l'implémentation.

Une échelle OKLCH vers hex pratique pour une teinte bleue :

Nuance | OKLCH                    | Hex
-------+--------------------------+--------
50     | oklch(0.97 0.04 250)     | #EFF6FF
100    | oklch(0.92 0.07 250)     | #DBEAFE
200    | oklch(0.84 0.11 250)     | #BFDBFE
300    | oklch(0.74 0.16 250)     | #93C5FD
400    | oklch(0.65 0.19 250)     | #60A5FA
500    | oklch(0.55 0.22 250)     | #3B82F6
600    | oklch(0.47 0.22 250)     | #2563EB
700    | oklch(0.40 0.20 250)     | #1D4ED8
800    | oklch(0.32 0.18 250)     | #1E40AF
900    | oklch(0.24 0.14 250)     | #1E3A8A
950    | oklch(0.17 0.10 250)     | #172554

Utilisez le Générateur de nuances de ColorFYI pour générer des échelles de nuances style Tailwind à partir de n'importe quelle couleur de base, et le Convertisseur de couleurs pour inspecter chaque nuance générée dans l'espace OKLCH et vérifier la progression de luminosité.

Combien de paliers de nuances ?

Pour la plupart des design systems, 9 à 11 paliers est la bonne fourchette :

  • 50 et 950 : valeurs extrêmes pour les fonds de surface et l'usage texte/bordure le plus profond.
  • 100–200 : fonds teintés clairs (fonds d'alerte, états de survol sur surfaces blanches).
  • 300–400 : couleurs d'icônes, texte de substitution, états désactivés.
  • 500 : souvent la couleur de marque « pure » — celle que les standards de la marque spécifient.
  • 600–700 : remplissages d'éléments interactifs (boutons, liens) où le contraste sur fond blanc est requis.
  • 800–900 : usage texte et texte sur fond coloré.

Générez des nuances pour chaque famille de teinte de votre palette : votre couleur de marque principale, une série de gris neutres et les couleurs d'accent éventuelles (rouge/erreur, vert/succès, jaune/avertissement, etc.).


Tokens de couleurs sémantiques

Les primitifs établis, définissez des tokens sémantiques qui associent les primitifs à des rôles. La granularité de ce mapping détermine la flexibilité du système.

Couleurs de surface

Les tokens de surface définissent les couches d'arrière-plan de votre interface :

:root {
  --color-surface-base: var(--neutral-50);       /* Fond de page */
  --color-surface-elevated: var(--white);         /* Fonds de cartes et de feuilles */
  --color-surface-overlay: var(--white);          /* Fonds de modales et popovers */
  --color-surface-sunken: var(--neutral-100);     /* Champs encastrés, blocs de code */
}

[data-theme="dark"] {
  --color-surface-base: var(--neutral-950);
  --color-surface-elevated: var(--neutral-900);
  --color-surface-overlay: var(--neutral-800);
  --color-surface-sunken: var(--neutral-950);
}

Couleurs de texte

Les tokens de texte couvrent chaque rôle de texte dans la hiérarchie :

:root {
  --color-text-primary: var(--neutral-900);      /* Texte de corps, titres */
  --color-text-secondary: var(--neutral-600);    /* Légendes, étiquettes */
  --color-text-tertiary: var(--neutral-400);     /* Texte de substitution, indications */
  --color-text-disabled: var(--neutral-300);     /* État désactivé */
  --color-text-inverse: var(--white);            /* Texte sur fonds sombres/colorés */
  --color-text-link: var(--blue-600);
  --color-text-link-hover: var(--blue-700);
}

[data-theme="dark"] {
  --color-text-primary: var(--neutral-50);
  --color-text-secondary: var(--neutral-400);
  --color-text-tertiary: var(--neutral-600);
  --color-text-disabled: var(--neutral-700);
  --color-text-link: var(--blue-400);
  --color-text-link-hover: var(--blue-300);
}

Couleurs d'action interactives

Les tokens d'action couvrent chaque état des éléments interactifs :

:root {
  /* Action principale */
  --color-action-primary-bg: var(--blue-600);
  --color-action-primary-bg-hover: var(--blue-700);
  --color-action-primary-bg-active: var(--blue-800);
  --color-action-primary-text: var(--white);

  /* Action secondaire / fantôme */
  --color-action-secondary-border: var(--blue-600);
  --color-action-secondary-text: var(--blue-600);
  --color-action-secondary-bg-hover: var(--blue-50);

  /* Action destructive */
  --color-action-danger-bg: var(--red-600);
  --color-action-danger-bg-hover: var(--red-700);
  --color-action-danger-text: var(--white);
}

Couleurs de retour / statut

Les tokens de statut associent les couleurs fonctionnelles à leur signification sémantique :

:root {
  /* Succès */
  --color-status-success-bg: var(--green-50);
  --color-status-success-border: var(--green-200);
  --color-status-success-text: var(--green-700);
  --color-status-success-icon: var(--green-500);

  /* Avertissement */
  --color-status-warning-bg: var(--yellow-50);
  --color-status-warning-border: var(--yellow-200);
  --color-status-warning-text: var(--yellow-800);
  --color-status-warning-icon: var(--yellow-500);

  /* Erreur */
  --color-status-error-bg: var(--red-50);
  --color-status-error-border: var(--red-200);
  --color-status-error-text: var(--red-700);
  --color-status-error-icon: var(--red-500);

  /* Information */
  --color-status-info-bg: var(--blue-50);
  --color-status-info-border: var(--blue-200);
  --color-status-info-text: var(--blue-700);
  --color-status-info-icon: var(--blue-500);
}

Couleurs de bordure

:root {
  --color-border-default: var(--neutral-200);
  --color-border-strong: var(--neutral-400);
  --color-border-focused: var(--blue-500);
  --color-border-error: var(--red-500);
  --color-border-disabled: var(--neutral-200);
}

[data-theme="dark"] {
  --color-border-default: var(--neutral-700);
  --color-border-strong: var(--neutral-500);
  --color-border-focused: var(--blue-400);
  --color-border-error: var(--red-400);
  --color-border-disabled: var(--neutral-700);
}

Implémentation en CSS

L'implémentation CSS utilise des propriétés personnalisées définies au niveau :root (ou sur des sélecteurs [data-theme] pour le thème). Les composants référencent des tokens sémantiques, jamais directement des primitifs.

Structure de fichiers

design-system/
├── tokens/
│   ├── primitives.css       # Toutes les valeurs de couleurs primitives
│   ├── semantic.css         # Mapping token sémantique → primitif
│   └── dark.css             # Substitutions sémantiques du mode sombre
└── components/
    ├── button.css
    ├── card.css
    └── ...

primitives.css

:root {
  /* Échelle neutre */
  --neutral-50: #F8FAFC;
  --neutral-100: #F1F5F9;
  --neutral-200: #E2E8F0;
  --neutral-300: #CBD5E1;
  --neutral-400: #94A3B8;
  --neutral-500: #64748B;
  --neutral-600: #475569;
  --neutral-700: #334155;
  --neutral-800: #1E293B;
  --neutral-900: #0F172A;
  --neutral-950: #020617;

  /* Échelle bleue */
  --blue-50: #EFF6FF;
  --blue-100: #DBEAFE;
  --blue-200: #BFDBFE;
  --blue-300: #93C5FD;
  --blue-400: #60A5FA;
  --blue-500: #3B82F6;
  --blue-600: #2563EB;
  --blue-700: #1D4ED8;
  --blue-800: #1E40AF;
  --blue-900: #1E3A8A;
  --blue-950: #172554;

  /* Blanc et noir */
  --white: #FFFFFF;
  --black: #000000;
}

Utilisation dans les composants

Les composants ne référencent que des tokens sémantiques :

.button-primary {
  background-color: var(--color-action-primary-bg);
  color: var(--color-action-primary-text);
  border: none;
}

.button-primary:hover {
  background-color: var(--color-action-primary-bg-hover);
}

.button-primary:active {
  background-color: var(--color-action-primary-bg-active);
}

.button-primary:disabled {
  background-color: var(--neutral-200);  /* Exception : l'état désactivé utilise souvent des primitifs */
  color: var(--color-text-disabled);
}

Quand le thème passe de clair à sombre, --color-action-primary-bg se résout automatiquement vers une valeur différente — aucune substitution de mode sombre spécifique aux boutons n'est nécessaire.


Implémentation dans Tailwind CSS

Tailwind v3 et v4 prennent en charge l'intégration de tokens de design via le fichier de configuration et l'approche par variables CSS.

Configuration Tailwind v3

Définissez votre échelle de couleurs dans tailwind.config.js et mappez-la vers des propriétés CSS personnalisées :

// tailwind.config.js
const colors = require("tailwindcss/colors");

module.exports = {
  theme: {
    colors: {
      transparent: "transparent",
      current: "currentColor",
      white: "#ffffff",
      black: "#000000",

      // Échelles primitives
      neutral: {
        50:  "#F8FAFC",
        100: "#F1F5F9",
        200: "#E2E8F0",
        300: "#CBD5E1",
        400: "#94A3B8",
        500: "#64748B",
        600: "#475569",
        700: "#334155",
        800: "#1E293B",
        900: "#0F172A",
        950: "#020617",
      },
      blue: {
        50:  "#EFF6FF",
        100: "#DBEAFE",
        200: "#BFDBFE",
        300: "#93C5FD",
        400: "#60A5FA",
        500: "#3B82F6",
        600: "#2563EB",
        700: "#1D4ED8",
        800: "#1E40AF",
        900: "#1E3A8A",
        950: "#172554",
      },

      // Tokens sémantiques via variables CSS
      "action-primary": "var(--color-action-primary-bg)",
      "action-primary-hover": "var(--color-action-primary-bg-hover)",
      "surface": "var(--color-surface-base)",
      "surface-elevated": "var(--color-surface-elevated)",
      "text-primary": "var(--color-text-primary)",
      "text-secondary": "var(--color-text-secondary)",
      "border-default": "var(--color-border-default)",
    },
  },
};

Avec cette configuration, vous pouvez écrire bg-action-primary et cela se résout vers la valeur correcte du token sémantique, qui à son tour se résout vers le primitif correct pour le thème actif.

Configuration CSS-first de Tailwind v4

Tailwind v4 remplace la configuration JavaScript par une approche CSS-first utilisant @theme :

@import "tailwindcss";

@theme {
  /* Couleurs primitives */
  --color-blue-500: #3B82F6;
  --color-blue-600: #2563EB;
  --color-blue-700: #1D4ED8;
  --color-neutral-50: #F8FAFC;
  --color-neutral-900: #0F172A;

  /* Tokens sémantiques */
  --color-surface: var(--color-surface-base);
  --color-text: var(--color-text-primary);
  --color-primary: var(--color-action-primary-bg);
}

/* Définitions sémantiques */
:root {
  --color-surface-base: var(--color-neutral-50);
  --color-text-primary: var(--color-neutral-900);
  --color-action-primary-bg: var(--color-blue-600);
}

[data-theme="dark"] {
  --color-surface-base: oklch(0.145 0 0);
  --color-text-primary: var(--color-neutral-50);
  --color-action-primary-bg: var(--color-blue-500);
}

Tests et documentation

Un système de couleurs n'est utile que s'il est compris et implémenté de façon fiable par toute l'équipe. Les tests détectent les régressions ; la documentation rend le système appréhendable.

Tests de contraste

Chaque paire token de texte / token de surface de fond prévu doit être vérifiée programmatiquement pour satisfaire aux exigences WCAG 2.1 AA (4,5:1 pour le texte normal, 3:1 pour le grand texte). Écrivez des tests qui :

  1. Résolvent les valeurs actuelles des propriétés CSS personnalisées (pour les thèmes clair et sombre).
  2. Calculent le ratio de contraste WCAG entre chaque paire texte/surface.
  3. Vérifient que le ratio satisfait au seuil requis.
import { getContrastRatio } from "some-color-utility";

const tokens = {
  light: {
    textPrimary: "#0F172A",
    surfaceBase: "#F8FAFC",
  },
  dark: {
    textPrimary: "#F8FAFC",
    surfaceBase: "#020617",
  }
};

describe("Contraste du système de couleurs", () => {
  test("text-primary sur surface-base passe AA en mode clair", () => {
    const ratio = getContrastRatio(tokens.light.textPrimary, tokens.light.surfaceBase);
    expect(ratio).toBeGreaterThanOrEqual(4.5);
  });

  test("text-primary sur surface-base passe AA en mode sombre", () => {
    const ratio = getContrastRatio(tokens.dark.textPrimary, tokens.dark.surfaceBase);
    expect(ratio).toBeGreaterThanOrEqual(4.5);
  });
});

Utilisez le Vérificateur de contraste de ColorFYI pour la vérification manuelle de paires de tokens individuelles lors de la définition des tokens, avant d'écrire des tests automatisés.

Documentation des tokens de couleurs

Documentez chaque token sémantique avec : - Nom : le nom exact de la variable CSS. - Valeur (clair) : le primitif qu'il référence en mode clair. - Valeur (sombre) : le primitif en mode sombre. - Usage : où ce token doit être utilisé. - Anti-patterns : où il ne doit pas être utilisé.

| Token                        | Clair          | Sombre         | Usage                              |
|------------------------------|----------------|----------------|------------------------------------|
| --color-text-primary         | neutral-900    | neutral-50     | Texte de corps, titres             |
| --color-text-secondary       | neutral-600    | neutral-400    | Légendes, métadonnées, étiquettes  |
| --color-action-primary-bg    | blue-600       | blue-500       | Fond du bouton principal           |
| --color-surface-elevated     | white          | neutral-900    | Fonds de cartes, feuilles, panneaux |

Tests de régression visuelle

Combinez la documentation des tokens avec des tests de régression visuelle à l'aide d'outils comme Percy, Chromatic ou la comparaison de captures d'écran de Playwright. Un test de régression visuelle rend votre bibliothèque de composants à chaque changement de token, capturant des captures d'écran et les comparant à une référence — tout changement de couleur déclenche une révision, détectant les régressions accidentelles.

Page de palette de couleurs Storybook

Dans Storybook, créez une story dédiée à la palette de couleurs qui rend tous les tokens sous forme de pastilles :

// ColorPalette.stories.jsx
const tokens = [
  { name: "--color-surface-base", label: "Surface / Base" },
  { name: "--color-surface-elevated", label: "Surface / Élevée" },
  { name: "--color-text-primary", label: "Texte / Principal" },
  // ...
];

export function ColorPaletteDoc() {
  return (
    <div>
      {tokens.map(({ name, label }) => (
        <div key={name} style={{ display: "flex", alignItems: "center", gap: 16 }}>
          <div style={{
            width: 48,
            height: 48,
            backgroundColor: `var(${name})`,
            borderRadius: 4,
            border: "1px solid rgba(0,0,0,0.1)"
          }} />
          <div>
            <div>{label}</div>
            <code>{name}</code>
          </div>
        </div>
      ))}
    </div>
  );
}

Cela rend les valeurs réelles des tokens en direct — dans le thème actif dans Storybook — rendant la documentation auto-vérifiée plutôt que statique.


Points clés

  • Un système de couleurs sépare les tokens primitifs (nommés par leur position dans une échelle : blue-600) des tokens sémantiques (nommés par leur rôle : color-action-primary-bg). Les composants référencent toujours des tokens sémantiques, jamais des primitifs.
  • Cette séparation permet le mode sombre et le thème en remappant les tokens sémantiques vers d'autres primitifs au niveau des propriétés CSS personnalisées, sans aucune modification du code des composants.
  • Générez des échelles de nuances (50–950) en utilisant l'espace colorimétrique OKLCH où le canal de luminosité est perceptuellement uniforme — des paliers numériques égaux produisent des résultats visuellement équidistants. Utilisez le Générateur de nuances de ColorFYI pour créer des échelles compatibles Tailwind à partir de n'importe quelle couleur d'ancrage.
  • Les catégories de tokens sémantiques couvrent quatre domaines : surface (couches de fond), texte (hiérarchie des rôles de texte), action (états des éléments interactifs) et retour (succès, avertissement, erreur, information).
  • En CSS, définissez les primitifs dans un fichier et les mappings sémantiques dans un autre. Utilisez les substitutions du sélecteur [data-theme="dark"] pour remapper les tokens sémantiques en mode sombre.
  • Dans Tailwind v3, étendez l'objet colors de la configuration avec des références à des propriétés CSS personnalisées. Dans Tailwind v4, utilisez le bloc @theme CSS-first pour définir les tokens primitifs et sémantiques.
  • Testez chaque paire token texte/surface pour la conformité au contraste WCAG — de façon programmatique, pas seulement manuellement. Utilisez le Vérificateur de contraste de ColorFYI pour la vérification manuelle lors de la définition des tokens.
  • Documentez les tokens dans Storybook avec des pastilles en direct qui rendent les valeurs réelles des propriétés CSS personnalisées, garantissant que la documentation reflète l'état actuel réel du système.

Couleurs associées

Marques associées

Outils associés