Tutoriels

Fonction CSS light-dark() : bascule de thème native

9 min de lecture

L'implémentation du mode sombre a historiquement nécessité une quantité non négligeable de code standard : une media query prefers-color-scheme qui surcharge chaque propriété personnalisée, du JavaScript pour gérer les bascules utilisateur, localStorage pour persister les préférences, et un script inline dans <head> pour éviter l'éclair du mauvais thème au chargement de la page. La fonction CSS light-dark() ne supprime pas tout cela, mais elle réduit considérablement la surface CSS du problème.

light-dark() est une fonction de couleur CSS qui prend exactement deux valeurs de couleur et retourne la première quand le schéma de couleurs actif est clair, ou la seconde quand il est sombre. C'est l'équivalent sémantique CSS de l'opérateur ternaire pour les couleurs.

Qu'est-ce que light-dark() ?

La signature de la fonction est simple :

color: light-dark(<light-color>, <dark-color>);

Quand le schéma de couleurs actif est clair, le navigateur utilise <light-color>. Quand il est sombre, il utilise <dark-color>. Le « schéma de couleurs actif » est déterminé par la propriété CSS color-scheme, qui à son tour répond à la media query système prefers-color-scheme ou à une valeur explicite définie sur un élément.

La fonction est supportée dans :

  • Chrome/Edge : depuis la version 123 (mars 2024)
  • Firefox : depuis la version 120 (novembre 2023)
  • Safari : depuis la version 17.5 (juin 2024)

Le support mondial est d'environ 85% début 2026. C'est un ajout relativement récent, mais la couverture des navigateurs augmente suffisamment vite pour l'utiliser en production avec une stratégie de fallback.

Comment fonctionne-t-il avec la propriété color-scheme

light-dark() ne fonctionne pas de façon isolée. Il dépend entièrement de la propriété CSS color-scheme correctement définie. Sans elle, la fonction n'a aucun contexte pour décider quelle valeur retourner.

La propriété color-scheme déclare quels schémas de couleurs un document ou un élément supporte. La définir sur :root est le point de départ :

:root {
  color-scheme: light dark;
}

Cette seule déclaration indique au navigateur que votre page supporte les schémas de couleurs clair et sombre. Le navigateur :

  1. Lit la préférence système prefers-color-scheme de l'utilisateur
  2. Applique le schéma correspondant
  3. Fait résoudre tous les appels light-dark() de la page à la valeur appropriée

Avec cela en place, définir des couleurs adaptées au thème devient une affaire de simples déclarations uniques :

:root {
  color-scheme: light dark;

  --color-background: light-dark(#FFFFFF, #0F0F17);
  --color-text:       light-dark(#1A1A2E, #E8E8F0);
  --color-border:     light-dark(#DEE2E6, #2E2E4A);
  --color-accent:     light-dark(#2563EB, #60A5FA);
}

Pas de media query. Pas de surcharges de sélecteurs. Une déclaration par couleur, les deux valeurs en ligne. Le navigateur gère la commutation automatiquement en fonction de la préférence système.

Restreindre à un seul schéma

Définir color-scheme: light ou color-scheme: dark force un seul schéma quelle que soit la préférence système :

/* Toujours clair, quelle que soit la préférence du système d'exploitation */
.widget {
  color-scheme: light;
  background: light-dark(#FFFFFF, #0F0F17);
  /* Se résout toujours à #FFFFFF */
}

/* Toujours sombre */
.dark-panel {
  color-scheme: dark;
  color: light-dark(#1A1A2E, #E8E8F0);
  /* Se résout toujours à #E8E8F0 */
}

C'est utile pour les composants d'interface qui doivent toujours apparaître dans un mode spécifique — par exemple, un éditeur de code qui doit toujours avoir un fond sombre quelle que soit le thème de la page environnante.

Le mot-clé only

Ajouter only empêche la cascade de remplacer le schéma pour cet élément :

.forced-light {
  color-scheme: only light;
}

C'est principalement utile quand vous avez un élément dans un contexte en mode sombre qui doit rester clair.

Remplacer les media queries prefers-color-scheme

L'approche traditionnelle du mode sombre avec les media queries nécessite de dupliquer ou de surcharger chaque variable de couleur :

/* Approche traditionnelle — verbeuse */
:root {
  --bg: #FFFFFF;
  --text: #1A1A2E;
  --accent: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0F0F17;
    --text: #E8E8F0;
    --accent: #60A5FA;
  }
}

Avec light-dark(), cela se réduit à :

/* Approche light-dark() — une déclaration par variable */
:root {
  color-scheme: light dark;

  --bg:     light-dark(#FFFFFF, #0F0F17);
  --text:   light-dark(#1A1A2E, #E8E8F0);
  --accent: light-dark(#2563EB, #60A5FA);
}

Les variables elles-mêmes deviennent auto-descriptives. Quand vous lisez --accent: light-dark(#2563EB, #60A5FA), vous voyez immédiatement les deux valeurs et comprenez la relation. L'approche par media query disperse les valeurs claires et sombres sur deux blocs séparés, ce qui rend l'audit et la mise à jour de la palette plus difficiles.

Quand les media queries sont encore nécessaires

La media query prefers-color-scheme reste nécessaire pour les adaptations non-couleur qui changent selon le thème :

@media (prefers-color-scheme: dark) {
  /* Ajustements non-couleur que light-dark() ne peut pas exprimer */
  img.logo {
    filter: invert(1) brightness(1.2);
  }

  .hero-image {
    opacity: 0.85;
  }
}

Pour tout ce qui est purement un changement de couleur, light-dark() est plus propre. Pour les adaptations structurelles ou non-couleur (filtres d'image, opacité, propriétés d'affichage), la media query reste le bon outil.

Combinaison avec les propriétés personnalisées CSS

light-dark() fonctionne à l'intérieur des valeurs de propriétés personnalisées, où sa pleine puissance émerge. Vous définissez toutes les couleurs adaptées au thème sur :root, et chaque composant de la page référence ces variables. Quand le schéma de couleurs change, tout se met à jour simultanément.

Exemple de système de thème complet

:root {
  color-scheme: light dark;

  /* Fonds */
  --color-bg-base:      light-dark(#FFFFFF,   #0F0F17);
  --color-bg-elevated:  light-dark(#F8F9FA,   #1A1A2E);
  --color-bg-overlay:   light-dark(#F1F3F5,   #252540);

  /* Texte */
  --color-text-primary:   light-dark(#1A1A2E, #E8E8F0);
  --color-text-secondary: light-dark(#4A4A6A, #A8A8C0);
  --color-text-muted:     light-dark(#6C757D, #6A6A88);
  --color-text-inverse:   light-dark(#FFFFFF, #1A1A2E);

  /* Bordures */
  --color-border:         light-dark(#DEE2E6, #2E2E4A);
  --color-border-strong:  light-dark(#ADB5BD, #4A4A6A);

  /* Interactif / marque */
  --color-accent:         light-dark(#2563EB, #60A5FA);
  --color-accent-hover:   light-dark(#1D4ED8, #93C5FD);
  --color-accent-subtle:  light-dark(#DBEAFE, #1E3A5F);

  /* Retour d'information */
  --color-success:  light-dark(#16A34A, #4ADE80);
  --color-warning:  light-dark(#D97706, #FCD34D);
  --color-danger:   light-dark(#DC2626, #F87171);

  --color-success-bg: light-dark(#F0FDF4, #052E16);
  --color-warning-bg: light-dark(#FFFBEB, #2D1A00);
  --color-danger-bg:  light-dark(#FEF2F2, #2D0A0A);
}

Les composants référencent ces variables sans avoir besoin de connaître quoi que ce soit sur la thématisation :

.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  color: var(--color-text-primary);
}

.btn-primary {
  background: var(--color-accent);
  color: var(--color-text-inverse);
}

.btn-primary:hover {
  background: var(--color-accent-hover);
}

.alert-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-left: 3px solid var(--color-success);
}

Notez que l'accent clair #2563EB passe à #60A5FA en mode sombre. C'est intentionnel — le bleu de poids 600 passe le contraste WCAG AA contre le blanc mais échoue contre les fonds sombres. Le poids 400 éclaircit suffisamment la couleur pour maintenir un contraste accessible sur les surfaces sombres. Utilisez le Vérificateur de contraste pour vérifier que chaque combinaison atteint votre ratio de contraste cible, et le Générateur de nuances pour trouver la bonne nuance de chaque couleur pour chaque mode.

Imbrication de light-dark() dans d'autres fonctions

light-dark() retourne une valeur de couleur, elle peut donc être utilisée partout où une couleur est valide — y compris à l'intérieur d'autres fonctions :

:root {
  color-scheme: light dark;
  --brand: #3B82F6;

  /* Utiliser light-dark() à l'intérieur de color-mix() */
  --brand-surface: color-mix(
    in oklch,
    var(--brand) 15%,
    light-dark(white, #09090b)
  );
}

Cela crée une couleur de surface qui est une teinte à 15% de la couleur de marque, mélangée avec du blanc en mode clair et du quasi-noir en mode sombre — automatiquement adaptée au thème.

Ajouter une bascule utilisateur (JavaScript)

Répondre à la préférence système est la bonne valeur par défaut, mais les utilisateurs doivent pouvoir la remplacer. Cela nécessite du JavaScript pour persister leur choix et remplacer le comportement par défaut du navigateur.

Contrôler color-scheme avec JavaScript

L'insight clé est que color-scheme est une propriété CSS qui peut être définie via JavaScript :

// Définir le schéma de couleurs de façon programmatique
document.documentElement.style.colorScheme = 'dark';
document.documentElement.style.colorScheme = 'light';

// Supprimer la surcharge (revient à la préférence système)
document.documentElement.style.colorScheme = '';

Quand vous définissez color-scheme via le style inline sur l'élément racine, cela remplace la déclaration de la feuille de style. Toutes les valeurs light-dark() se résolvent à nouveau à la variante appropriée.

Implémentation complète de la bascule

const STORAGE_KEY = 'color-scheme-preference';

function initColorScheme() {
  const stored = localStorage.getItem(STORAGE_KEY);
  if (stored === 'light' || stored === 'dark') {
    document.documentElement.style.colorScheme = stored;
  }
  // Si aucune préférence sauvegardée, le CSS color-scheme: light dark; gère via la préférence OS
}

function toggleColorScheme() {
  const current = getComputedStyle(document.documentElement)
    .colorScheme
    .trim();

  // Déterminer la valeur suivante
  const next = current.includes('dark') ? 'light' : 'dark';

  document.documentElement.style.colorScheme = next;
  localStorage.setItem(STORAGE_KEY, next);

  // Mettre à jour l'état du bouton de bascule
  updateToggleButton(next);
}

function updateToggleButton(scheme) {
  const btn = document.getElementById('theme-toggle');
  if (!btn) return;
  btn.setAttribute('aria-label',
    scheme === 'dark' ? 'Passer au mode clair' : 'Passer au mode sombre'
  );
  btn.dataset.scheme = scheme;
}

// Exécuter avant le premier rendu pour éviter l'éclair
initColorScheme();

// Attacher au bouton de bascule une fois que le DOM est prêt
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('theme-toggle')
    ?.addEventListener('click', toggleColorScheme);
});

Appeler initColorScheme() avant que le DOM ne soit complètement analysé est crucial. S'il s'exécute trop tard, les utilisateurs avec une préférence sauvegardée verront brièvement le thème par défaut du système avant qu'il ne bascule — le classique « éclair du mauvais thème ». Placez ce script inline dans <head> ou utilisez l'attribut defer avec soin (notez que les scripts defer s'exécutent après l'analyse du DOM, ce qui peut être trop tard).

Le schéma anti-éclair

L'approche anti-éclair la plus robuste exécute un script inline minimal dans <head> :

<head>
  <meta name="color-scheme" content="light dark">
  <script>
    const stored = localStorage.getItem('color-scheme-preference');
    if (stored) {
      document.documentElement.style.colorScheme = stored;
    }
  </script>
  <link rel="stylesheet" href="styles.css">
</head>

Ce script s'exécute de façon synchrone avant qu'aucun CSS ne soit appliqué, de sorte que le navigateur calcule la valeur correcte de color-scheme dès le premier rendu. La balise <meta name="color-scheme"> indique au navigateur quels schémas attendre même avant que le CSS ne soit analysé — cela évite un bref éclair blanc sur les pages en mode sombre dans certains navigateurs.

Guide de migration depuis des thèmes basés sur JavaScript

De nombreuses implémentations de mode sombre existantes utilisent un attribut data-theme basculé par JavaScript, avec des surcharges CSS délimitées à [data-theme="dark"]. La migration vers light-dark() est incrémentale — vous n'avez pas besoin de tout changer d'un coup.

Étape 1 : ajouter color-scheme à :root

:root {
  color-scheme: light dark;
  /* Les propriétés personnalisées existantes restent inchangées */
}

Étape 2 : migrer les variables une par une

Commencez par une seule variable comme preuve de concept. Remplacez le schéma de déclaration divisée par un light-dark() unifié :

/* Avant */
:root {
  --bg: #FFFFFF;
}
[data-theme="dark"] {
  --bg: #0F0F17;
}

/* Après */
:root {
  color-scheme: light dark;
  --bg: light-dark(#FFFFFF, #0F0F17);
}

Étape 3 : mettre à jour la bascule

Modifiez la bascule JavaScript pour définir style.colorScheme au lieu de data-theme :

/* Avant */
document.documentElement.setAttribute('data-theme', scheme);

/* Après */
document.documentElement.style.colorScheme = scheme;

Étape 4 : supprimer les sélecteurs data-theme

Une fois toutes les variables migrées, supprimez les blocs CSS [data-theme="dark"].

Maintenir les deux systèmes pendant la transition

Vous pouvez exécuter les deux systèmes simultanément. Conservez les surcharges [data-theme="dark"] pour les variables pas encore migrées. Les nouvelles variables utilisent light-dark(). La bascule JavaScript définit à la fois data-theme et style.colorScheme pendant la période de transition.

Fallback pour le support navigateur

Pour les environ 15% de navigateurs sans support de light-dark(), fournissez des fallbacks explicites :

:root {
  /* Fallback : valeurs explicites en mode clair */
  --color-bg: #FFFFFF;
  --color-text: #1A1A2E;

  /* Amélioration progressive avec light-dark() */
  --color-bg:   light-dark(#FFFFFF, #0F0F17);
  --color-text: light-dark(#1A1A2E, #E8E8F0);
}

/* Fallback mode sombre pour les navigateurs sans light-dark() */
@supports not (color: light-dark(white, black)) {
  @media (prefers-color-scheme: dark) {
    :root {
      --color-bg: #0F0F17;
      --color-text: #E8E8F0;
    }
  }
}

Le bloc @supports not (color: light-dark(white, black)) ne s'applique qu'aux navigateurs qui ne comprennent pas light-dark(). Les navigateurs modernes le sautent entièrement car la condition négative est fausse.

Points clés à retenir

  • light-dark(<light-value>, <dark-value>) retourne le premier argument dans un schéma de couleurs clair et le second dans un schéma sombre. C'est la façon native CSS d'exprimer « cette couleur, adaptée au thème actuel. »
  • Elle dépend de la propriété CSS color-scheme définie sur l'élément (ou un ancêtre). Définissez toujours color-scheme: light dark sur :root pour activer l'adaptation automatique via prefers-color-scheme.
  • L'avantage principal par rapport à l'approche traditionnelle par media query est de colocaliser les deux valeurs de thème dans une seule déclaration — rendant la relation entre les variantes claires et sombres explicite et auditable.
  • Les surcharges utilisateur nécessitent de définir document.documentElement.style.colorScheme via JavaScript. Persistez le choix dans localStorage et appliquez-le dans un script inline dans <head> avant le chargement du CSS pour éviter l'éclair.
  • La migration depuis des systèmes basés sur l'attribut data-theme est incrémentale — déplacez une variable à la fois du schéma de surcharge [data-theme="dark"] vers le schéma inline light-dark().
  • Le support navigateur est d'environ 85% en 2026. Fournissez un fallback @supports not avec un bloc @media (prefers-color-scheme: dark) pour les environnements plus anciens.
  • Utilisez le Vérificateur de contraste pour vérifier que les valeurs de couleur claires et sombres dans chaque paire light-dark() respectent les exigences de contraste WCAG contre leurs fonds respectifs, et le Générateur de nuances pour trouver la bonne nuance de chaque couleur pour chaque mode.

Couleurs associées

Marques associées

Outils associés