Tutoriels

Gestion des thèmes de couleurs en React : variables CSS et Context

11 min de lecture

Le thème est l'un de ces problèmes qui semblent simples jusqu'à ce que vous deviez le maintenir à grande échelle. Un simple basculement entre mode clair et mode sombre est direct. Un produit qui sert plusieurs marques, offre des palettes personnalisables par l'utilisateur ou doit changer de thème instantanément sans rechargement de page nécessite une architecture plus délibérée.

Le modèle de composant React et les propriétés CSS personnalisées sont une association naturelle pour le thème. Les variables CSS gèrent les valeurs de couleur de façon déclarative ; React Context gère l'état du thème ; et Tailwind CSS (dans les projets modernes) fait le pont entre les deux. Ce guide couvre chaque couche — comment structurer l'architecture de thème, implémenter les fondations en variables CSS, connecter l'état React et gérer les scénarios multi-marques.


Patterns d'architecture de thème

Pattern 1 : variables CSS uniquement (sans état JavaScript)

Le pattern le plus simple pour la prise en charge basique du mode sombre ne nécessite aucun état React. Une media query prefers-color-scheme change les valeurs de propriétés personnalisées, et chaque composant se met à jour automatiquement :

/* globals.css */
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0F172A;
    --color-text: #F1F5F9;
    --color-brand: #60A5FA;
  }
}

Les composants utilisent var(--color-bg) et n'ont jamais besoin de connaître le thème actuel. Le navigateur gère tout.

Quand l'utiliser : les sites où vous souhaitez respecter la préférence du système d'exploitation et où vous n'avez pas besoin d'un basculement contrôlé par l'utilisateur.

Limitation : aucun moyen pour les utilisateurs de remplacer leur paramètre d'OS dans l'application.

Pattern 2 : basculement de thème par attribut data

Ajoutez un attribut data-theme sur l'élément <html> pour rendre le thème actif explicite et modifiable. Ce pattern est compatible avec le SSR, évite le flash de mauvais thème et se combine trivialement avec la persistance dans localStorage :

/* Par défaut : clair */
[data-theme="light"],
:root {
  --color-bg: #F8FAFC;
  --color-text: #1E293B;
  --color-brand: #2563EB;
  --color-surface: #FFFFFF;
  --color-border: #E2E8F0;
}

/* Sombre */
[data-theme="dark"] {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
  --color-brand: #60A5FA;
  --color-surface: #1E293B;
  --color-border: #334155;
}
// Appliquer le thème avant le premier rendu (script inline dans <head>)
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);

Quand l'utiliser : la plupart des applications de production qui nécessitent un basculement contrôlé par l'utilisateur. L'approche par attribut data est la norme actuelle du secteur — utilisée par l'implémentation du mode sombre de Tailwind CSS, Radix UI et la plupart des design systems modernes.

Pattern 3 : basculement de thème par classe

Similaire à l'attribut data, mais utilise des classes CSS. La configuration darkMode: 'class' de Tailwind s'appuie sur ce pattern :

.dark {
  --color-bg: #0F172A;
  --color-text: #F1F5F9;
}

Ajouter la classe dark à <html> active les valeurs du token sombre. Tailwind applique alors les utilitaires préfixés dark: quand cette classe est présente.

Quand l'utiliser : les projets utilisant Tailwind CSS avec la stratégie de mode sombre class.


Les variables CSS pour les couleurs : le système de tokens

Nommage sémantique plutôt que descriptif

La décision la plus importante dans un système de tokens est le nommage. Nommer par valeur de couleur (--blue-500, --gray-900) rend la variable facile à comprendre isolément mais impossible à thématiser — changer --blue-500 en violet casse la sémantique du nom.

Nommer par rôle sémantique (--color-brand, --color-text-muted) permet aux valeurs de changer entièrement entre les thèmes tandis que les composants restent corrects :

:root {
  /* ---- Primitifs de couleur (non utilisés directement dans les composants) ---- */
  --blue-500: #3B82F6;
  --blue-700: #1D4ED8;
  --slate-50: #F8FAFC;
  --slate-800: #1E293B;
  --slate-900: #0F172A;

  /* ---- Tokens sémantiques (utilisés dans les composants) ---- */
  /* Texte */
  --text-primary: var(--slate-800);
  --text-secondary: #64748B;
  --text-disabled: #94A3B8;
  --text-inverse: var(--slate-50);

  /* Surfaces */
  --bg-base: var(--slate-50);
  --bg-elevated: #FFFFFF;
  --bg-sunken: #F1F5F9;

  /* Marque */
  --brand: var(--blue-700);
  --brand-hover: #1E40AF;
  --brand-subtle: #EFF6FF;
  --on-brand: #FFFFFF;

  /* Retour */
  --error: #DC2626;
  --error-bg: #FEF2F2;
  --success: #16A34A;
  --success-bg: #F0FDF4;
  --warning: #D97706;
  --warning-bg: #FFFBEB;
}

[data-theme="dark"] {
  --text-primary: #F1F5F9;
  --text-secondary: #94A3B8;
  --text-disabled: #475569;
  --text-inverse: #0F172A;

  --bg-base: #0F172A;
  --bg-elevated: #1E293B;
  --bg-sunken: #0D1426;

  --brand: #60A5FA;
  --brand-hover: #93C5FD;
  --brand-subtle: #172554;
  --on-brand: #0F172A;

  --error: #F87171;
  --error-bg: #1C0A0A;
  --success: #4ADE80;
  --success-bg: #052E16;
  --warning: #FCD34D;
  --warning-bg: #1C1000;
}

Le système à deux couches (primitifs + tokens sémantiques) vous donne le meilleur des deux mondes : une palette de couleurs brute pour référence, et des tokens sémantiques pour l'utilisation dans les composants.

Générer votre échelle de couleurs

Avant de construire le système de tokens, vous avez besoin de la palette de couleurs brute. Le Générateur de nuances produit une échelle complète de 50 à 950 à partir d'une seule couleur de marque — le même pattern d'échelle utilisé par Tailwind CSS. Entrez le code hex de votre marque et obtenez l'ensemble complet des variantes du plus sombre au plus clair, prêtes à utiliser comme tokens primitifs.

Par exemple, en entrant #2563EB comme bleu de marque, on génère :

  • blue-50: #EFF6FF
  • blue-100: #DBEAFE
  • blue-500: #3B82F6
  • blue-700: #1D4ED8
  • blue-900: #1E3A8A
  • blue-950: #172554

Ces valeurs primitives alimentent ensuite vos tokens sémantiques, garantissant que vos couleurs de mode sombre sont harmoniquement liées à vos couleurs de mode clair plutôt que des approximations sombres arbitraires.


React Context pour l'état du thème

Le pattern ThemeContext

Pour les applications avec un basculement de thème contrôlé par l'utilisateur, React Context fournit la couche de gestion d'état. Le context stocke le nom du thème actif et expose une fonction de basculement :

// contexts/ThemeContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextValue {
  theme: Theme;
  resolvedTheme: 'light' | 'dark'; // Thème réellement appliqué (résout 'system')
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setThemeState] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'system';
    return (localStorage.getItem('theme') as Theme) || 'system';
  });

  const systemPrefersDark = typeof window !== 'undefined'
    ? window.matchMedia('(prefers-color-scheme: dark)').matches
    : false;

  const resolvedTheme: 'light' | 'dark' =
    theme === 'system'
      ? (systemPrefersDark ? 'dark' : 'light')
      : theme;

  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme', resolvedTheme);
    localStorage.setItem('theme', theme);
  }, [theme, resolvedTheme]);

  // Écouter les changements de préférence système quand theme === 'system'
  useEffect(() => {
    if (theme !== 'system') return;
    const media = window.matchMedia('(prefers-color-scheme: dark)');
    const handler = () => {
      document.documentElement.setAttribute(
        'data-theme',
        media.matches ? 'dark' : 'light'
      );
    };
    media.addEventListener('change', handler);
    return () => media.removeEventListener('change', handler);
  }, [theme]);

  const setTheme = (newTheme: Theme) => {
    setThemeState(newTheme);
  };

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme(): ThemeContextValue {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme doit être utilisé dans ThemeProvider');
  return ctx;
}

Le ThemeProvider enveloppe votre application (ou le sous-arbre concerné) et le hook useTheme expose l'état du thème n'importe où dans l'arbre de composants :

// components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      aria-label={`Passer en mode ${theme === 'dark' ? 'clair' : 'sombre'}`}
      style={{
        background: 'var(--bg-elevated)',
        color: 'var(--text-primary)',
        border: '1px solid var(--color-border)',
        padding: '8px 16px',
        borderRadius: '8px',
        cursor: 'pointer',
      }}
    >
      {theme === 'dark' ? '☀ Clair' : '☾ Sombre'}
    </button>
  );
}

Les composants utilisent des propriétés CSS personnalisées pour leurs styles visuels — ils n'ont pas besoin de lire resolvedTheme pour appliquer les couleurs. Le context n'est nécessaire que pour les UI qui affichent ou contrôlent l'état actuel du thème.

Prévenir le flash de mauvais thème (FOTWT)

Les applications rendues côté serveur font face au problème du flash de mauvais thème : le serveur rend le HTML sans connaître la préférence de thème de l'utilisateur, le navigateur affiche ce HTML momentanément, puis React s'hydrate et applique le bon thème — provoquant un flash visible.

La solution est un script inline bloquant dans <head> qui lit localStorage et applique l'attribut de thème avant que la page se rende :

<!-- Dans votre <head>, avant tout lien CSS -->
<script>
  (function() {
    try {
      var stored = localStorage.getItem('theme');
      var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      var theme = stored === 'dark' || stored === 'light'
        ? stored
        : (systemDark ? 'dark' : 'light');
      document.documentElement.setAttribute('data-theme', theme);
    } catch(e) {}
  })();
</script>

Dans Next.js, ce code va dans _document.tsx ou dans le layout.tsx racine de l'App Router. Comme ce script s'exécute de façon synchrone avant que tout CSS soit analysé, il n'y a aucun flash.


Thèmes Tailwind CSS

Tailwind v3 : mode sombre basé sur les classes

Dans Tailwind v3, le mode sombre nécessite de définir darkMode: 'class' dans tailwind.config.js. Cela fait en sorte que Tailwind applique les utilitaires dark: seulement quand la classe .dark est sur l'élément <html> :

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#EFF6FF',
          100: '#DBEAFE',
          500: '#3B82F6',
          600: '#2563EB',
          700: '#1D4ED8',
          900: '#1E3A8A',
          950: '#172554',
        },
      },
    },
  },
};

Les composants utilisent les préfixes dark: pour les variantes de mode sombre :

<div className="bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100">
  <button className="bg-brand-600 dark:bg-brand-500 text-white hover:bg-brand-700 dark:hover:bg-brand-400">
    Action principale
  </button>
</div>

Le basculement de thème définit la classe .dark sur document.documentElement — la même approche que le pattern par attribut data, mais en utilisant une classe plutôt qu'un attribut.

Tailwind v4 : configuration CSS-first

Tailwind v4 déplace entièrement la configuration dans le CSS, en utilisant nativement les propriétés CSS personnalisées :

/* styles.css */
@import "tailwindcss";

@theme {
  --color-brand-50: #EFF6FF;
  --color-brand-100: #DBEAFE;
  --color-brand-500: #3B82F6;
  --color-brand-600: #2563EB;
  --color-brand-700: #1D4ED8;
}

Tailwind v4 génère des classes utilitaires à partir de ces propriétés personnalisées, et elles sont immédiatement utilisables en JSX : className="bg-brand-600 text-white". La configuration du mode sombre dans v4 utilise @variant dark :

@variant dark (&:where([data-theme="dark"] *)) {
  /* Tailwind applique les utilitaires dark: selon ce sélecteur */
}

Cette variante indique à Tailwind d'appliquer les utilitaires dark: quand un ancêtre possède data-theme="dark", s'intégrant nativement avec le pattern par attribut data.


Stratégie de thème multi-marques

Le défi

Une plateforme SaaS servant plusieurs clients, un produit en marque blanche ou un design system partagé entre plusieurs marques de produit doit gérer non seulement les variantes clair/sombre mais des identités de couleur entièrement différentes — chacune avec sa propre couleur principale de marque, ses accents, ses couleurs de statut et ses neutres.

Les tokens de marque comme substitutions de variables CSS

L'approche la plus maintenable définit des tokens sémantiques agnostiques à la marque dans une feuille de style de base, puis remplace les assignations de primitifs par marque :

/* Thème de base — mêmes noms de tokens pour toutes les marques */
:root {
  /* Primitifs de marque — remplacés par marque */
  --brand-primary-raw: 37 99 235; /* #2563EB en canaux RGB */
  --brand-accent-raw: 99 102 241; /* #6366F1 en canaux RGB */

  /* Tokens sémantiques — calculés à partir des primitifs */
  --brand-primary: rgb(var(--brand-primary-raw));
  --brand-primary-hover: color-mix(in srgb, rgb(var(--brand-primary-raw)) 80%, black);
  --brand-primary-subtle: color-mix(in srgb, rgb(var(--brand-primary-raw)) 10%, white);
}

/* Marque : Acme Corp (bleu) */
[data-brand="acme"] {
  --brand-primary-raw: 37 99 235;   /* #2563EB */
  --brand-accent-raw: 16 185 129;   /* #10B981 */
}

/* Marque : Globex Inc (violet) */
[data-brand="globex"] {
  --brand-primary-raw: 124 58 237;  /* #7C3AED */
  --brand-accent-raw: 245 158 11;   /* #F59E0B */
}

/* Marque : Initech (vert) */
[data-brand="initech"] {
  --brand-primary-raw: 22 163 74;   /* #16A34A */
  --brand-accent-raw: 239 68 68;    /* #EF4444 */
}

La technique des canaux RGB bruts (37 99 235) permet des variantes d'opacité sans tokens supplémentaires :

.overlay {
  background-color: rgba(var(--brand-primary-raw), 0.1);
}

Context React multi-marques

Étendez le context de thème pour gérer la marque :

// contexts/ThemeContext.tsx
type Brand = 'acme' | 'globex' | 'initech' | 'default';
type Theme = 'light' | 'dark';

interface ThemeContextValue {
  theme: Theme;
  brand: Brand;
  setTheme: (theme: Theme) => void;
  setBrand: (brand: Brand) => void;
}

export function ThemeProvider({ initialBrand = 'default', children }: {
  initialBrand?: Brand;
  children: React.ReactNode;
}) {
  const [theme, setTheme] = useState<Theme>('light');
  const [brand, setBrand] = useState<Brand>(initialBrand);

  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute('data-theme', theme);
    root.setAttribute('data-brand', brand);
  }, [theme, brand]);

  return (
    <ThemeContext.Provider value={{ theme, brand, setTheme, setBrand }}>
      {children}
    </ThemeContext.Provider>
  );
}

Définissez la marque initiale depuis votre couche de routage ou d'authentification — le sous-domaine ou l'identifiant locataire de chaque client correspond à un identifiant de marque.

Générer des échelles de marque avec le Générateur de nuances

Pour chaque marque, vous avez besoin d'une échelle de couleurs complète, pas seulement d'une ou deux valeurs hex. Utilisez le Générateur de nuances pour générer une échelle de 50 à 950 pour les couleurs principale et d'accent de chaque marque. Entrez le hex principal de la marque et obtenez la gamme complète des variantes du plus sombre au plus clair.

Pour Globex Inc avec la couleur principale #7C3AED, le générateur de nuances produit l'échelle de violet complète. Pour Initech avec #16A34A, l'échelle de vert complète. Ces échelles alimentent ensuite les tokens primitifs pour chaque marque, garantissant que --brand-primary-subtle (la teinte la plus claire) et --brand-primary-hover (un état pressé plus sombre) restent harmoniquement cohérents au sein de chaque identité de marque.


Points clés

  • Les propriétés CSS personnalisées sont la fondation : définissez des tokens sémantiques (--text-primary, --brand-primary) et changez leurs valeurs par thème, plutôt que d'appliquer conditionnellement des classes dans chaque composant.
  • Le basculement de thème par attribut data (data-theme="dark") est le pattern le plus flexible — il est modifiable par JavaScript, compatible avec le SSR et combinable avec les media queries prefers-color-scheme.
  • Le système de tokens à deux couches (échelle de couleurs primitive + tokens de rôle sémantique) permet une flexibilité complète de thème tout en gardant les composants propres : les primitifs définissent votre palette, les sémantiques définissent comment les couleurs sont utilisées.
  • Utilisez le Générateur de nuances pour générer une échelle de couleurs complète de 50 à 950 à partir de votre couleur principale de marque — cela vous donne la gamme complète de variantes claires et sombres nécessaires pour un système de tokens complet.
  • React Context gère l'état du thème (le nom du thème actif) et les effets de bord (définir l'attribut data, persister dans localStorage) ; les composants n'ont jamais besoin de lire le context pour appliquer les couleurs correctes.
  • Prévenir le flash de mauvais thème avec un script inline bloquant dans <head> qui lit localStorage et applique l'attribut de thème avant que le navigateur rende le moindre pixel.
  • Intégration Tailwind CSS : v3 utilise darkMode: 'class' avec les préfixes dark: ; v4 utilise la configuration CSS-first @theme avec support natif des variables CSS.
  • Le thème multi-marques empile un autre attribut data (data-brand) sur le système de thème, remplaçant les valeurs de tokens primitifs par marque tout en gardant les tokens sémantiques et les composants inchangés.

Couleurs associées

Marques associées

Outils associés