Mis à jour 11 juillet 2026

prefers-color-scheme : bien l’utiliser en CSS

prefers-color-scheme est la fonctionnalité média CSS qui signale le schéma de couleurs demandé par l’utilisateur — light ou dark — tel qu’il est défini dans le système d’exploitation ou le navigateur. Bien l’utiliser signifie s’en servir uniquement comme valeur par défaut : des propriétés personnalisées définissent le thème clair sur :root, la media query inverse ces valeurs par défaut quand le système préfère le sombre, et un attribut data-theme posé par un bouton manuel surcharge les deux. La media query seule n’est pas une implémentation, car la plupart des produits réels ont aussi besoin du bouton — et un bouton ne peut pas surcharger une media query depuis le CSS.

Ceci est la référence d’implémentation pour la mécanique de bascule de notre guide du mode sombre : la query, le motif de surcharge, et les quatre pièges qui remplissent les forums d’entraide.

Que lit réellement la media query ?

La préférence exprimée par l’utilisateur, transmise depuis l’environnement : le réglage d’apparence du système d’exploitation (ou une surcharge au niveau du navigateur là où elle existe) apparaît en CSS comme une fonctionnalité média, exactement comme la largeur du viewport. La forme la plus simple :

@media (prefers-color-scheme: dark) {
  /* styles pour les utilisateurs qui ont demandé le sombre */
}

Aucun JavaScript n’est en jeu, et la query est vivante : modifiez le réglage du système et les styles correspondants s’appliquent immédiatement. Les valeurs de la fonctionnalité et les notes de compatibilité sont sur la page de référence de MDN. Ce que la query ne connaît pas, c’est votre interface — elle rapporte l’état du système, pas le bouton dans votre en-tête.

Pourquoi un bouton manuel doit-il surcharger la media query ?

Parce que tout produit réel finit avec trois états, pas deux : suivre le système, clair forcé et sombre forcé. Un utilisateur dont le système est en sombre peut malgré tout vouloir votre application en clair (les longues sessions de lecture en sont une raison courante), et inversement. La media query ne peut exprimer que le premier état — le motif robuste superpose donc un attribut par-dessus :

:root {
  color-scheme: light dark;
  --background:   oklch(0.94 0.008 262.9);
  --text-primary: oklch(0.28 0.02 262.9);
}

/* par défaut : suivre le système */
@media (prefers-color-scheme: dark) {
  :root {
    --background:   oklch(0.22 0.015 262.9);
    --text-primary: oklch(0.94 0.008 262.9);
  }
}

/* états forcés : l'attribut l'emporte sur la valeur par défaut */
[data-theme="light"] {
  color-scheme: light;
  --background:   oklch(0.94 0.008 262.9);
  --text-primary: oklch(0.28 0.02 262.9);
}

[data-theme="dark"] {
  color-scheme: dark;
  --background:   oklch(0.22 0.015 262.9);
  --text-primary: oklch(0.94 0.008 262.9);
}

body { background: var(--background); color: var(--text-primary); }

Lisez-le comme une cascade de valeurs par défaut : :root dit clair, la media query dit « sauf si le système préfère le sombre », l’attribut dit « sauf si l’utilisateur nous a dit autrement ». Sans attribut, le site suit le système ; avec un attribut, le choix in-app de l’utilisateur l’emporte. Côté JavaScript, c’est une ligne plus le stockage :

const saved = localStorage.getItem("theme");   // "light" | "dark" | null
if (saved) document.documentElement.dataset.theme = saved;

null — le troisième état — signifie « ne poser aucun attribut et laisser la media query décider ». Stocker explicitement "system" et retirer l’attribut fonctionne de la même manière.

Voilà l’exemple complet : environ vingt-cinq lignes de CSS et trois de JavaScript couvrent le suivi du système, les deux états forcés et les contrôles de formulaire natifs. Les valeurs elles-mêmes vont par paires — une par thème, par rôle — et c’est là que la couche de tokens fournit ses données au motif.

Voir un ensemble de rôles portant les valeurs des deux thèmes dans Scale Composer — chaque rôle sémantique avec sa valeur claire et sa valeur sombre côte à côte, prêtes à coller dans les deux blocs ci-dessus.

Une liste de rôles sémantiques dans Scale Composer montrant les valeurs claires et sombres dans des colonnes parallèles pour les mêmes noms de rôles

Quels sont les pièges qui remplissent les forums d’entraide ?

Les contrôles de formulaire ignorent votre thème. Votre CSS peint la page en sombre, mais les barres de défilement, cases à cocher et menus déroulants restent clairs — parce que c’est le navigateur qui les peint, et on ne lui a rien dit. Le correctif est la ligne déjà présente dans le motif ci-dessus : color-scheme: light dark indique au navigateur quels schémas la page prend en charge, et les blocs d’état forcé le restreignent pour que les contrôles natifs suivent la surcharge, pas seulement la query.

Le flash du mauvais thème. Un utilisateur qui préfère le sombre charge la page, voit un flash blanc, puis le thème s’applique. Cela arrive quand la surcharge stockée est appliquée par un script qui s’exécute après le premier rendu. Le correctif honnête est un script inline dans le <head> — avant le rendu dépendant de la feuille de style, ni différé, ni intégré au bundle — qui lit localStorage et pose l’attribut. Il bloque le rendu par conception ; limitez-le à ces quelques lignes.

Les images et les intégrations ne suivent pas. Un élément <picture> peut changer de source selon le thème avec media="(prefers-color-scheme: dark)", mais cela suit le système, pas votre surcharge data-theme — une asymétrie connue du motif à attribut. Les iframes résolvent la query dans leur propre contexte. Pour un contenu qui doit suivre le bouton in-app, c’est au CSS (ou à un petit script) de faire le changement.

La bascule anime tout. Un transition: all général sur les éléments concernés par le thème transforme le basculement en un lent fondu enchaîné de toutes les couleurs à l’écran. Ciblez les transitions sur les propriétés qui en ont besoin, ou désactivez brièvement les transitions pendant le changement.

La query rend-elle le thème sombre réussi ?

Non — elle répond seulement à quel thème afficher. Que le thème sombre vaille la peine d’y basculer se décide par la palette derrière les propriétés personnalisées : un jeu sombre dérivé avec sa propre courbe de luminosité et un renfort de chroma se lit comme un thème conçu, tandis que des valeurs claires inversées se lisent comme le négatif d’un thème. La query fait le routage ; les valeurs sont le produit. Dériver ces valeurs est le sujet du reste de ce hub.

Vérifiez le motif avec vos propres valeurs

Les deux blocs du motif ne valent que ce que valent les paires dont vous les remplissez. Chargez une palette et vérifiez l’ancre de chaque rôle sous les deux thèmes — les mêmes noms de rôles se résolvant par thème, les planchers de contraste revérifiés de chaque côté, pour que les valeurs que vous collez dans la media query et dans le bloc d’attribut soient déjà des paires vérifiées.

Continuer la lecture

  • Tokens sémantiques pour le changement de thème

    Les design tokens du mode sombre expliqués : des rôles sémantiques qui portent une valeur par thème, pourquoi le mapping sombre n'est pas un miroir, et le CSS qui change de thème.

  • Le mode sombre n'est pas une inversion

    La conception en mode sombre commence là où l'inversion échoue : logique des rôles renversée, élévation cassée, couleur vidée. Ce que signifie dériver une palette sombre à partir des mêmes couleurs de départ.