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.

RM
Rabie Menad
3 min de lecture

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

OutilCe qu'il vérifieVerdict
TypeScriptLes types, pas les chaînes CSSMuet
ESLintLes règles de codeMuet
TailwindLe token existe, la classe est généréeMuet
Le buildLa compilation aboutitMuet

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).fontSize

Trente secondes. C'est le prix de la certitude, et il est très inférieur à celui du doute.

RM
Rabie MenadDéveloppeur freelance

Je conçois et livre des produits web performants en React & Next.js. Parlons de votre projet.

Plus d'articles