La classe Tailwind qui ne s'appliquait jamais
Tailwind la générait, TypeScript la validait, ESLint ne disait rien. Elle n'avait simplement aucun effet. Petit rappel que la seule preuve est dans le navigateur.
En ajoutant une échelle typographique personnalisée à ce site, j'ai écrit une classe qui n'a jamais produit le moindre pixel de différence. Aucun outil ne s'en est plaint. Le bug n'a été visible qu'en inspectant le DOM.
Le point de départ
Trois tailles de police revenaient en dur dans le code : text-[11px], text-[10px],
text-[0.8125rem]. L'objectif était banal : les remplacer par des tokens du design system.
Tailwind 4 se configure directement en CSS, ce qui rend l'ajout 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 à l'écran. Le sommaire, censé afficher ses sous-titres en 13px, restait obstinément à 14px.
La classe était pourtant bien présente dans le JSX :
className={cn(
'text-sm transition-colors',
entry.level === 3 && 'text-xs-plus ps-6',
)}Ce que j'ai cru comprendre
Ma première hypothèse était que tailwind-merge supprimait la classe, la prenant pour un
doublon de text-sm. Logique, et fausse.
Le DOM racontait autre chose :
"relative flex items-baseline ... transition-colors text-sm ps-6"
ps-6 était bien là, donc la condition s'évaluait correctement. Mais text-xs-plus avait
disparu, et text-sm était resté.
La vérification qui tranche
Plutôt que de continuer à supposer, un test direct de la bibliothèque :
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 est le comportement attendu : text-2xs remplace text-sm. La
première ne supprime rien du tout : elle conserve les deux classes.
La vraie explication
tailwind-merge doit savoir que deux classes appartiennent au même groupe pour arbitrer
entre elles. Il reconnaît les motifs standard de l'échelle (xs, sm, 2xs, 3xs), mais
xs-plus ne ressemble à rien de connu. Ne pouvant l'identifier comme une taille de police,
il le laisse passer aux côtés de text-sm.
Les deux classes se retrouvent donc dans le DOM, avec une spécificité CSS identique. C'est
l'ordre dans la feuille de style qui tranche, 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 dès qu'on utilise cn(), donc sur tout 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.
C'est contre-intuitif : on croit nommer pour des humains, alors qu'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 nécessaire : 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 largement Tailwind. Une suite verte prouve que le code compile, pas qu'il produit le résultat attendu. Pour du CSS, la vérification tient en une ligne :
getComputedStyle(element).fontSizeTrente secondes. C'est le prix de la certitude, et il est très inférieur à celui du doute.
Plus d'articles

De la startup à l'entreprise : ce que 5 pays m'ont appris
Passer de la startup aux grands comptes m'a appris une chose : la vraie galère n'est pas dans le code, elle est dans le "blast radius".
5 min de lecture
Next ou TanStack ? La question n'est pas là
J'utilise les deux. Mon portfolio tourne sous Next, mon SaaS sous TanStack, et les deux sont en production. Ce qui compte, c'est ce qui part en prod.
4 min de lecture