La classe Tailwind qui ne s'appliquait jamais
Tailwind la générait, TypeScript la validait, ESLint ne disait rien. Elle n'avait juste aucun effet. Le bug venait du nom que j'avais choisi.
En ajoutant une échelle typographique à ce site, j'ai écrit une classe qui n'a jamais rien changé à l'écran.
Aucun outil ne s'en est plaint. Tout était vert. Il a fallu ouvrir l'inspecteur pour comprendre.
Le point de départ
Trois tailles de police traînaient en dur dans le code : text-[11px], text-[10px], text-[0.8125rem]. Objectif banal : les remplacer par des tokens du design system.
Tailwind 4 se configure directement en CSS, donc l'ajout est trivial :
@theme inline {
--text-3xs: 0.625rem;
--text-2xs: 0.6875rem;
--text-xs-plus: 0.8125rem;
}Trois tokens, trois remplacements. pnpm typecheck, pnpm lint:ci, pnpm test, pnpm build : tout au vert.
Le symptôme
Un seul des trois ne changeait rien. Le sommaire, censé afficher ses sous-titres en 13px, restait à 14px.
La classe était pourtant bien dans le JSX :
className={cn(
'text-sm transition-colors',
entry.level === 3 && 'text-xs-plus ps-6',
)}Ma première hypothèse, et pourquoi elle était fausse
J'ai d'abord pensé à tailwind-merge, qui aurait supprimé la classe en la prenant pour un doublon de text-sm.
Logique. Et faux.
Le DOM racontait autre chose :
"relative flex items-baseline ... transition-colors text-sm ps-6"
ps-6 était là, donc la condition s'évaluait bien. Mais text-xs-plus avait disparu et text-sm était resté. Si tailwind-merge avait fait son travail de déduplication, c'est l'inverse qui aurait dû se produire.
Le test qui tranche
Plutôt que de continuer à supposer, j'ai interrogé la bibliothèque directement :
node -e "
const { twMerge } = require('tailwind-merge');
console.log(twMerge('text-sm', 'text-xs-plus'));
console.log(twMerge('text-sm', 'text-2xs'));
"Résultat :
text-sm text-xs-plus
text-2xs
La deuxième ligne montre le comportement attendu : text-2xs remplace text-sm.
La première ne supprime rien. Elle garde les deux classes.
L'explication
tailwind-merge doit reconnaître que deux classes appartiennent au même groupe pour arbitrer entre elles. Il connaît les motifs standard de l'échelle (xs, sm, 2xs, 3xs), mais xs-plus ne ressemble à rien qu'il sache classer. Ne pouvant l'identifier comme une taille de police, il le laisse passer à côté de text-sm.
Les deux se retrouvent donc dans le DOM, avec la même spécificité CSS. C'est l'ordre dans la feuille de style qui décide, et Tailwind trie ses utilitaires par taille croissante. text-sm (14px) étant généré après text-xs-plus (13px), il gagne.
Le bug ne venait ni de Tailwind, ni de
tailwind-merge. Il venait du nom que j'avais choisi.
Le correctif
Renommer le token pour qu'il suive un motif reconnu :
/* `2sm` (et non `xs-plus`) : tailwind-merge ne traite que la forme
`<n>xs` / `<n>sm` comme une taille de police */
--text-2sm: 0.8125rem;
--text-2sm--line-height: 1.5;Vérification dans le navigateur, cette fois :
getComputedStyle(h3).fontSize // "13px"
getComputedStyle(h2).fontSize // "14px"Ce que j'en retiens
Un nom n'est jamais neutre
Dès qu'on utilise tailwind-merge, donc cn(), donc n'importe quel projet shadcn/ui, le nom d'un token custom devient une contrainte technique. Il doit rester dans les motifs que la bibliothèque sait classer.
On croit nommer pour des humains. On nomme aussi pour un parseur.
Aucun outil ne pouvait le voir
| Outil | Ce qu'il vérifie | Verdict |
|---|---|---|
| TypeScript | Les types, pas les chaînes CSS | Muet |
| ESLint | Les règles de code | Muet |
| Tailwind | Le token existe, la classe est générée | Muet |
| Le build | La compilation aboutit | Muet |
Chacun faisait correctement son travail. Aucun n'avait le contexte : la question n'était pas « cette classe existe-t-elle ? » mais « cette classe l'emporte-t-elle ? ».
Seul le navigateur tranche
C'est la vraie leçon, et elle dépasse Tailwind. Une suite verte prouve que le code compile, pas qu'il fait ce qu'on croit.
Pour du CSS, la vérification tient en une ligne :
getComputedStyle(element).fontSizeTrente secondes. C'est moins cher que le doute.
Plus d'articles

De la startup à l'entreprise : ce que 7 pays m'ont appris
Deux ans chez Decathlon à livrer dans 7 pays. Ce qui m'a marqué, ce n'est pas le code : c'est ce qui casse quand on se trompe.
8 min de lecture
Next ou TanStack ? La question n'est pas là
J'ai passé des heures à comparer des frameworks. Aucune ne m'a rapporté un client. Ce qui compte dans un SaaS, c'est la distribution, et ça se prépare avant la première ligne de code.
7 min de lecture