Mis à jour 11 juillet 2026

Tokens sémantiques pour le changement de thème

Le changement de thème est un problème de nommage avant d’être un problème de CSS. Les design tokens du mode sombre sont des rôles sémantiques — background, surface, text-primary — où chaque rôle porte deux valeurs, une par thème, et les composants ne référencent que le nom du rôle. Changez de thème et chaque référence se ré-résout d’un coup ; aucun composant n’a d’opinion sur le mode dans lequel il se trouve. Le risque d’ingénierie n’est pas le changement mais le mapping : la table rôle-vers-valeur d’un thème sombre n’est pas un miroir de celle du thème clair, elle doit donc être dérivée et revérifiée, jamais inversée.

La structure en couches en dessous tient en un paragraphe : les tokens primitifs nomment la matière première (les échelons de la rampe — brand-600, neutral-100), les tokens sémantiques nomment des tâches d’UI et pointent vers les primitifs, et les composants ne consomment que la sémantique. Les arguments de nommage et la mécanique du fichier de design ont leurs propres guides ; cet article — qui fait partie de notre guide du mode sombre — porte sur ce que fait la couche sémantique quand un second thème arrive.

Pourquoi chaque rôle a-t-il besoin de deux valeurs ?

Parce que le sens d’un rôle est stable et sa valeur ne l’est pas. background signifie « la couleur de repos de la page » dans n’importe quel thème ; la valeur qui remplit ce rôle est presque blanche en mode clair et presque noire en mode sombre. La couche sémantique est la frontière du thème : tout ce qui se trouve au-dessus — composants, mises en page, écrans — reste aveugle au thème, et tout ce qui se trouve en dessous change en bloc.

C’est aussi la raison intuitive pour laquelle le motif fonctionne pour ceux qui l’utilisent. Un designer ou un développeur qui raisonne en rôles se demande à quoi sert cette surface, ce qui a une seule réponse ; raisonner en valeurs revient à demander quel gris est-ce, ce qui a deux réponses qu’il faut synchroniser à la main. Des noms qui promettent une tâche plutôt qu’une valeur, voilà ce qui fait du second thème un changement de données plutôt qu’un second design.

Pourquoi le mapping sombre n’est-il pas un miroir du clair ?

Le modèle mental tentant consiste à inverser l’échelle — si le mode clair utilise neutral-100 pour la page, le mode sombre utilise son numéro opposé. Dériver le mapping puis le vérifier produit quelque chose de plus intéressant. Six rôles issus d’une palette générée à partir de #2563eboklch(0.546 0.215 262.9) :

RôleClairSombre (dérivé)Ce qu’un miroir direct dirait
backgroundneutral-100 (L ≈0.94)neutral-900 (L ≈0.22)neutral-800 — mais la page s’ancre au bas de l’échelle pour que l’échelle d’élévation ait de la place au-dessus d’elle
surface (carte)neutral-50 (L ≈0.97)neutral-800 (L ≈0.28)neutral-900 — ce qui ferait couler la carte sous la page et inverserait l’élévation
text-primaryneutral-800 (L ≈0.28)neutral-100 (L ≈0.94)neutral-100 — tient, même si les thèmes ajustés à la main adoucissent souvent trop vers neutral-200, qui paraît fin
text-secondaryneutral-600 (L ≈0.47)neutral-300 (L ≈0.78)neutral-300 — le miroir tient
border-subtleneutral-200, plus sombre que la carteneutral-700, plus clair que la carteles mêmes échelons — mais la relation change de sens
accentbrand-600 (≈#3E65B5)oklch(0.70 0.14 262.9) de la rampe sombrebrand-300 de la rampe claire — la mauvaise rampe entièrement

Trois asymétries méritent d’être nommées. Premièrement, background et surface croisent leurs miroirs : les surfaces surélevées doivent être plus claires que ce sur quoi elles reposent en mode sombre, si bien que la carte se place au-dessus de la page sur l’échelle de luminosité alors qu’un miroir l’aurait placée en dessous. Deuxièmement, le texte : neutral-200 sur la page sombre franchit encore le plancher arithmétique (≈12:1 contre l’exigence de 4,5:1), mais le texte clair sur fond sombre a tendance à paraître plus fin que ne le suggèrent les chiffres — APCA, qui modélise la polarité, rapporte pour cette paire une valeur inférieure à son jumeau du thème clair — de sorte que la dérivation atterrit un échelon plus clair que ce que la prudence choisirait. Troisièmement, la valeur sombre de l’accent ne provient pas du tout de la rampe claire : une palette sombre dérivée suit sa propre courbe de luminosité avec un chroma augmenté d’environ 20 %, parce qu’un environnement sombre atténue la coloration perçue.

La conséquence pratique : les deux mappings devraient être produits par une seule dérivation, et non maintenus comme deux listes. L’export de tokens de Scale Composer comporte une section semantic et une section semantic-dark générées à partir des mêmes couleurs de départ — chaque rôle redérivé par thème, les planchers WCAG revérifiés, l’APCA rapporté en parallèle, et les rôles onFill revérifiés, puisqu’un fond qui portait du texte blanc en clair peut vouloir du texte sombre en sombre.

Ouvrez l’export avec les deux sections sémantiques dans Scale Composer — les mêmes noms de rôle des deux côtés, les valeurs sombres visiblement pas un miroir des claires.

Export de tokens montrant les sections semantic et semantic-dark côte à côte, les mêmes noms de rôle se résolvant en valeurs différentes et asymétriques

Quels motifs de changement de thème échouent à grande échelle ?

Trois motifs reviennent, et tous les trois échouent de la même façon — le mapping se disperse au lieu d’être déclaré une seule fois.

Des variantes dark: par composant. Le préfixe dark: de Tailwind redéclare la valeur sombre à chaque point d’utilisation : bg-white dark:bg-gray-900 sur chaque carte, dans chaque fichier. Pour un petit site, ça va — tout le mapping tient sur un écran. Pour un système, c’est une taxe de maintenance : changer ce que signifie « surface » en mode sombre revient à un rechercher-remplacer dans toute la base de code, et tout composant qui a oublié son jumeau dark: est livré cassé dans un thème.

if (isDark) dans le code du composant. Les conditions de thème déplacent le mapping dans la logique, où il peut se ramifier, dériver et échapper à la relecture. Un composant qui demande quel thème est actif a pris en charge une responsabilité que la couche de tokens porte déjà.

Deux feuilles de style. Un dark.css maintenu à côté d’un light.css commence comme une copie et dérive dès la première modification qui ne touche qu’un seul fichier. Rien ne garantit que les deux fichiers répondent aux mêmes questions.

Comment les propriétés personnalisées CSS portent-elles le changement ?

En redéclarant les mêmes noms sous une portée de thème. Les composants référencent la propriété personnalisée ; la valeur de la propriété dépend de la déclaration qui s’applique à ce moment — la mécanique se résume aux propriétés personnalisées CSS plus la cascade, rien de plus.

:root {
  --color-background:   oklch(0.94 0.008 262.9);  /* neutral-100 */
  --color-surface:      oklch(0.97 0.005 262.9);  /* neutral-50  */
  --color-text-primary: oklch(0.28 0.02 262.9);   /* neutral-800 */
  --color-accent:       oklch(0.546 0.215 262.9); /* brand core  */
}

[data-theme="dark"] {
  --color-background:   oklch(0.22 0.015 262.9);  /* neutral-900 */
  --color-surface:      oklch(0.28 0.02 262.9);   /* neutral-800 */
  --color-text-primary: oklch(0.94 0.008 262.9);  /* neutral-100 */
  --color-accent:       oklch(0.70 0.14 262.9);   /* dark ramp   */
}

.card {
  background: var(--color-surface);
  color: var(--color-text-primary);
}

La règle .card est l’essentiel : elle apparaît une seule fois, ne mentionne aucun thème, et est correcte dans les deux. Le bloc sombre peut tout aussi bien s’accrocher à la media query prefers-color-scheme, ou — le motif robuste — la requête définit la valeur par défaut et l’attribut la remplace ; les détails du câblage font l’objet de leur propre article. Quel que soit le déclencheur choisi, l’export CSS depuis une source de tokens génère les deux blocs à partir de la même dérivation, et c’est ce qui empêche les deux mappings de s’écarter l’un de l’autre.

Regardez un changement se résoudre

Le mapping est plus facile à croire après l’avoir vu bouger. Basculez l’aperçu du thème sur l’ensemble complet des rôles — chaque rôle sémantique se ré-résolvant d’un coup, les asymétries du tableau ci-dessus visibles sur place : la carte restant plus claire que la page, l’accent sautant vers un échelon plus clair d’une rampe redérivée, aucune décision au niveau du composant nulle part.

Continuer la lecture

  • 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.

  • prefers-color-scheme : bien l'utiliser en CSS

    Comment fonctionne prefers-color-scheme : la media query comme valeur par défaut, une surcharge data-theme pour le bouton manuel, color-scheme, et le correctif du flash de thème incorrect.