Pourquoi CasT
La plupart des méthodologies de design tokens reposent sur une chaîne de traitement : un fichier JSON, un outil de build, une couche JavaScript
qui régénère du CSS. Ces outils ont leur place, mais pour une interface Web, le CSS dispose déjà d'un mécanisme conçu pour porter
des décisions de design : les variables CSS (propriétés personnalisées).
CasT n'est ni un outil ni une bibliothèque, mais une convention : une façon de nommer, ranger et faire circuler les tokens en
s'appuyant uniquement sur ce que le navigateur sait déjà faire : la cascade, l'héritage, les couches @layer et, quand c'est utile,
@property.
L'idée en une phrase : un token stabilise une primitive ou une décision d'usage, et c'est le navigateur, pas un outil externe, qui
la fait cascader, hériter et se surcharger.
Les 6 principes
1. Un token stabilise une décision, pas une valeur isolée
Toutes les valeurs ne méritent pas un nom partagé : un token vaut la peine d'exister quand la valeur doit être partagée,
adaptée, documentée ou surchargée. Les tokens globaux nomment des primitives stabilisées, comme une entrée de palette
ou d'échelle, tandis que les tokens sémantiques nomment une décision d'usage. Un détail de mise en page purement local peut rester
une valeur brute dans sa règle.
Un token sémantique que personne ne consomme est un signal d'alerte. Une entrée de palette inutilisée, en revanche, peut
être légitime : la base publie aussi ce qui servira aux variantes ou aux thèmes futurs.
2. Trois niveaux, une seule direction de dépendance
Chaque token appartient à l'un de ces trois niveaux, et le niveau se lit dans le nom, avec une dépendance qui ne va
jamais que dans un sens : composant → sémantique → global.
-
Global
La palette brute du système : couleurs, espacements, rayons.
-
Sémantique
L'intention d'usage, indépendante de la valeur brute.
--color-accent
--color-danger
-
Composant
Ce qu'un composant précis accepte de laisser personnaliser.
--button-bg
--field-border
Pour les couleurs et les intentions visuelles, un composant passe par un token sémantique : --button-bg peut
s'alimenter avec --color-danger, jamais avec --blue-600. Les primitives neutres comme l'espace,
le rayon, la taille de police ou la durée peuvent rester consommées directement quand elles ne cachent pas une intention d'usage.
L'anti-pattern à reconnaître tient en une ligne : .button { background: var(--blue-600); }. Rien n'est faux
tant qu'il n'y a qu'un thème, jusqu'au jour où un second arrive et oblige à rouvrir chaque composant pour aller y corriger une
couleur, alors qu'un seul token sémantique aurait suffi.
3. La cascade fait le travail, pas vous
Une variable CSS hérite, se redéfinit sur un élément et se propage à ses descendants sans qu'aucune règle ne soit réécrite.
Changer un thème, une zone ou un état revient à redéfinir quelques tokens au plus près du besoin, pas à dupliquer des propriétés
de rendu.
.danger-zone {
--button-bg: var(--color-danger);
}
Même dans une surcharge, --button-bg reste alimenté par un token sémantique, jamais par une valeur brute ni par un token
global de couleur consommé directement.
4. Des couches explicites pour arbitrer
Les couches @layer réduisent les guerres de spécificité et le recours aux !important. Une base publie des valeurs
par défaut dans une couche faible, un thème ou une application les surcharge dans une couche plus forte, sans alourdir les sélecteurs.
@layer reset, base, theme, components;
@layer base {
:root {
--blue-600: #155eef;
--green-600: #0e7c66;
--color-accent: var(--blue-600);
}
}
@layer theme {
[data-theme="calm"] { --color-accent: var(--green-600); }
}
La base publie toute la palette disponible, y compris les couleurs réservées aux thèmes alternatifs. Le thème ne fait que rebrancher
le token sémantique sur une entrée existante, il ne déclare jamais de nouvelle couleur lui-même.
La couche reset sert aux dépendances externes. Une bibliothèque importée sans couche reste hors couche, et une
règle hors couche l'emporte sur toutes les couches : le projet perdrait l'arbitrage face à sa propre dépendance. La ranger
explicitement suffit à inverser le rapport de force.
@import "une-bibliotheque.css" layer(reset);
La limite à garder en tête : cela ne marche que sur du CSS dont vous contrôlez l'import. Une bibliothèque qui injecte
sa feuille de styles à l'exécution, comme le font beaucoup de Web Components, arrive hors couche et l'emporte donc sur
tout ce que vous avez rangé. Face à ce cas, la seule position gagnante est de sortir la surcharge des couches elle aussi.
5. Un fallback local avant une dépendance globale
Un composant réutilisable déclare ses tokens avec un fallback dans var(). Il reste utilisable si rien n'est fourni,
et une variante ne surcharge que ce qui change réellement.
.badge {
--badge-bg: var(--color-surface-raised, Canvas);
background: var(--badge-bg);
}
La chaîne de fallbacks se lit de gauche à droite : le token sémantique du projet s'il existe, sinon la couleur système
Canvas, qui suit déjà le thème du navigateur. Le composant ne casse jamais, même déposé dans une page qui ne
connaît aucun de ses tokens.
6. @property pour les cas qui l'exigent, pas par défaut
Quand un token doit être validé, isolé de l'héritage ou animé, @property permet de lui donner une syntaxe, une valeur initiale et un
comportement d'héritage explicites. Pour une animation, elle donne au navigateur assez d'information pour interpoler la valeur ; une
propriété personnalisée classique reste opaque. Pour le reste (couleurs, espacements, valeurs statiques), une variable CSS classique suffit.
@property --notice-border-width {
syntax: "<length>";
inherits: false;
initial-value: 1px;
}
Une réserve à connaître avant de la mêler au principe 4 : @property n'est pas soumise aux couches. La ranger dans un
@layer ne l'affaiblit ni ne la renforce, et une seconde déclaration du même nom écrase la première quel que soit
l'ordre déclaré. Mieux vaut la garder hors couche, à un seul endroit.
Convention de nommage
Le nom seul doit dire à quel niveau appartient un token, sans avoir à remonter au :root pour vérifier.
- Global : nomme l'entrée de palette ou d'échelle :
--{échelle}-{valeur}, comme --blue-600 ou --radius-md.
- Sémantique : nomme l'usage, jamais la couleur :
--color-accent plutôt que --color-blue. Si le bleu devient vert, le nom doit rester vrai.
- Composant : préfixé par le nom du composant :
--button-bg, --notice-icon-color. Ne dépasse jamais la frontière du composant.
- Précision proportionnée :
--space-3 se lit d'un coup d'œil. --layout-density-action-inline demande un effort qui n'est justifié que si le token est réellement partagé à grande échelle.
- Préfixe si vous publiez : les propriétés personnalisées partagent un espace de noms global.
--blue-600 convient dans une application close, mais dès que le CSS est distribué ou cohabite avec une bibliothèque, un préfixe (--acme-blue-600) limite le risque de collisions silencieuses entre tokens.
Exemple complet
Les trois niveaux, une couche de thème, et un composant qui les consomme.
@layer base, theme, components;
@layer base {
:root {
--blue-600: #155eef;
--green-600: #0e7c66;
--space-3: 1.2rem;
--radius-md: 0.4rem;
--color-accent: var(--blue-600);
}
}
@layer theme {
[data-theme="calm"] { --color-accent: var(--green-600); }
}
@layer components {
.button {
--button-bg: var(--color-accent);
--button-radius: var(--radius-md);
background: var(--button-bg);
border-radius: var(--button-radius);
padding: var(--space-3) 1rem;
}
}
Dark mode : un thème, pas un composant à part
Le dark mode n'introduit aucune règle nouvelle dans les composants : il ajoute seulement une valeur au niveau sémantique, si bien que les tokens de composants n'ont jamais
besoin de savoir qu'un thème sombre existe.
Résoudre chaque paire dans le token lui-même
light-dark() déclare les deux valeurs d'un token sémantique au même endroit. Le navigateur choisit laquelle appliquer selon la valeur calculée de
color-scheme sur l'élément, sans media query à écrire.
:root {
color-scheme: light dark;
--white: #ffffff;
--gray-100: #f3f4f6;
--gray-200: #e4e6eb;
--gray-800: #2a2d34;
--gray-900: #1a1d23;
--gray-950: #16181d;
--blue-600: #155eef;
--blue-400: #6f9bff;
--color-text: light-dark(var(--gray-900), var(--gray-100));
--color-surface: light-dark(var(--white), var(--gray-950));
--color-border: light-dark(var(--gray-200), var(--gray-800));
--color-accent: light-dark(var(--blue-600), var(--blue-400));
}
light-dark() accepte n'importe quelle valeur de couleur, y compris un var(), rien n'oblige à y écrire des valeurs brutes. Chaque paire
reste une entrée de palette normale, au même niveau que le reste du global.
La limite est dans le nom : light-dark() ne résout que des couleurs, si bien qu'un token qui porte une ombre complète, un dégradé
ou une bordure entière ne peut pas l'utiliser. Deux issues : ne faire porter au token que la partie colorée
(box-shadow: 0 1px 3px var(--color-shadow)), ou redéfinir ce token-là dans la couche theme.
Dans cette approche, color-scheme: light dark suffit à laisser light-dark() suivre prefers-color-scheme. C'est le principe 3
(la cascade fait le travail) appliqué au thème plutôt qu'à une zone.
Forcer un choix manuel
Un sélecteur de thème n'a besoin de toucher qu'une seule propriété. color-scheme hérite : la redéfinir sur html fait basculer tous
les light-dark() descendants, sans reformuler un seul token.
@layer theme {
html[data-theme="dark"] { color-scheme: dark; }
html[data-theme="light"] { color-scheme: light; }
}
document.documentElement.setAttribute("data-theme", userChoice);
JavaScript pose un attribut, le CSS applique toutes les conséquences visuelles.
Sans light-dark() : la version media query
Pour une prise en charge plus large, le même résultat s'obtient en redéfinissant les tokens sémantiques deux fois : une fois pour la préférence système,
une fois pour le choix manuel, dans la couche theme.
@layer theme {
@media (prefers-color-scheme: dark) {
:root {
--color-text: var(--gray-100);
--color-surface: var(--gray-950);
}
}
[data-theme="dark"] {
--color-text: var(--gray-100);
--color-surface: var(--gray-950);
}
}
La palette (--gray-100, --gray-950,...) reste celle déclarée dans la base, et seule la couche theme décide quelle entrée s'applique.
Ce qui ne change jamais : button { --button-bg: var(--color-accent); }. Le composant ne référence que la couche sémantique, il traverse les deux
thèmes sans une seule ligne dédiée au dark mode. C'est le bénéfice concret de ne jamais laisser un composant consommer directement un token global de couleur (principe 2).
Web Components : le token est l'API
Une propriété personnalisée traverse la frontière du Shadow DOM. C'est même à peu près la seule chose qui la traverse :
un sélecteur écrit dans la page n'atteint pas l'intérieur d'un composant, une variable héritée si.
Le composant déclare donc, à l'intérieur de son shadow root, les tokens qu'il accepte de laisser personnaliser, chacun avec
son fallback local (principe 5). La page les redéfinit depuis l'extérieur, sans rien savoir de son balisage ni de sa spécificité.
button {
background: var(--switcher-bg, var(--color-surface-raised, Canvas));
color: var(--switcher-color, var(--color-text, CanvasText));
}
browserux-theme-switcher {
--switcher-bg: var(--color-surface);
--switcher-color: var(--color-accent);
}
C'est là que la méthode se distingue le plus nettement d'un pipeline de tokens : le contrat de personnalisation est déjà écrit
dans le langage. Pas de propriétés à propager, pas de thème à injecter, pas de build à relancer pour changer une couleur.
Le corollaire : un token de composant fait partie de son contrat public. Le renommer casse les pages qui le
surchargent, exactement comme un changement de signature. C'est une raison de plus de n'exposer, comme le dit le principe 1,
que ce qui mérite vraiment de l'être.
Limites
CasT organise des tokens, il ne garantit pas une interface réussie. Un nom bien choisi ne corrige pas un contraste insuffisant ni une échelle typographique incohérente.
La méthode aide à faire circuler les décisions, mais elle ne dispense pas de mesurer les contrastes, les états et les combinaisons réellement affichées.
Et si un système doit alimenter iOS, Android, Figma et le Web à la fois, un pipeline de tokens plus large reste nécessaire. CasT décrit ce qui se passe une fois
dans le navigateur, pas avant.
Un skill pour l'appliquer
Une convention sans vérification mécanique dérive, non par négligence, mais parce que l'essentiel ne se relit pas à l'œil :
qu'un token sémantique ait bien sa contrepartie sombre, qu'une bibliothèque importée ne batte pas les couches du projet,
qu'un texte à 1,12:1 de contraste soit invisible et non simplement discret.
D'où cascade-tokens, un skill qui outille la méthode plutôt que de la répéter, en fonctionnant dans les deux sens : partir
d'un projet vierge, ou reprendre un CSS existant dont les couleurs sont recopiées partout et dont le thème sombre ne tient qu'à moitié.
Cinq fichiers Python, sans aucune dépendance : quatre outils en ligne de commande et la bibliothèque partagée sur laquelle ils reposent.
Le dossier suit le standard Agent Skills : un SKILL.md, des scripts et des références à côté.
Claude Code et Codex savent lire ce format, chacun avec ses propres conventions d'installation
et d'invocation.
1. Les scripts
-
Proposer
Relève les valeurs récurrentes et en déduit une palette, des intentions sémantiques et des tokens de composant.
-
Auditer
Couleurs en dur, imports hors couche, sémantiques sans paire sombre, @property mal placée.
-
Mesurer
Résout var() et light-dark(), puis contrôle les contrastes dans les deux thèmes, à deux seuils.
Un quatrième, check_build_layers.py, travaille sur le CSS construit et non sur la source : les
@import y sont aplatis, et c'est ce fichier-là qui décide en production. C'est lui qui attrape une bibliothèque restée hors
couche, ou une surcharge rangée dans une couche où elle perdra.
Le cinquième fichier, cast_lib.py, ne s'appelle jamais directement, mais il tourne à chaque exécution : les quatre
autres l'importent. Il porte ce dont ils ont tous besoin, un petit scanner CSS, la conversion des couleurs, et la résolution
de var() et light-dark(). Sans lui, chaque script embarquerait son propre parseur, quatre au total,
qui divergeraient, et un bug corrigé dans l'un survivrait dans les trois autres.
Il est aussi importable de l'extérieur : un contrôle ponctuel gagne à s'écrire par-dessus plutôt qu'à refaire un parseur.
2. Installation
Le dépôt est le skill : le SKILL.md est à sa racine. Il suffit de le cloner directement
dans le dossier attendu par votre outil, git crée le répertoire au bon nom.
git clone https://github.com/Effeilo/cascade-tokens.git ~/.claude/skills/cascade-tokens
git clone https://github.com/Effeilo/cascade-tokens.git ~/.codex/skills/cascade-tokens
Pour ne l'installer que sur un projet plutôt que globalement, cloner dans
.claude/skills/cascade-tokens ou .codex/skills/cascade-tokens à la racine
de ce projet. C'est le même dépôt dans les deux cas, aucune adaptation du skill n'est nécessaire.
Sans git : le bouton Download ZIP de GitHub donne un dossier nommé
cascade-tokens-main. C'est le nom du dossier que lit l'assistant, renommez-le donc
cascade-tokens avant de le déposer.
3. Le déclencher
Deux façons. Explicitement, en citant son nom selon la convention de l'outil : c'est la voie la plus sûre quand on sait déjà
ce qu'on veut.
/cascade-tokens audite src/styles/ avant la refonte
$cascade-tokens audite src/styles/ avant la refonte
Ou sans rien invoquer : le skill décrit lui-même les situations qui le concernent, et l'assistant s'en saisit quand la
demande y correspond. « Mes couleurs sont en dur partout », « le dark mode est illisible », « je voudrais organiser mes
variables CSS » suffisent à le faire entrer en jeu.
Les scripts s'utilisent aussi seuls, sans passer par un assistant. À lancer depuis le dépôt cloné,
ou en pointant vers l'endroit où vous l'avez installé.
python3 scripts/suggest_tokens.py ~/mon-projet/src/styles/ --only-new
python3 scripts/audit_tokens.py ~/mon-projet/src/styles/
python3 scripts/check_contrast.py ~/mon-projet/src/styles/ --min 4.5 --min-ui 3
Le dépôt du skill est disponible sur GitHub :
Effeilo/cascade-tokens.git.
Ce qu'il ne fait pas : choisir votre identité visuelle, ni réécrire un CSS en une passe silencieuse. Il mesure,
propose avec les déclarations qui justifient chaque déduction, et laisse trancher. Un rapport qu'on ne peut pas contredire n'est
pas un diagnostic.