The Tailwind class that never applied
Tailwind generated it, TypeScript validated it, ESLint said nothing. It simply had no effect. The bug was in the name I had chosen.
While adding a type scale to this site, I wrote a class that never changed a single pixel on screen.
No tool complained. Everything was green. I had to open the inspector to understand.
The starting point
Three font sizes were sitting hard-coded across the codebase: text-[11px], text-[10px], text-[0.8125rem]. Mundane goal: replace them with design system tokens.
Tailwind 4 is configured directly in CSS, so adding them is trivial:
@theme inline {
--text-3xs: 0.625rem;
--text-2xs: 0.6875rem;
--text-xs-plus: 0.8125rem;
}Three tokens, three replacements. pnpm typecheck, pnpm lint:ci, pnpm test, pnpm build: all green.
The symptom
Only one of the three changed nothing. The table of contents, meant to render its sub-headings at 13px, stayed at 14px.
The class was right there in the JSX:
className={cn(
'text-sm transition-colors',
entry.level === 3 && 'text-xs-plus ps-6',
)}My first hypothesis, and why it was wrong
I assumed tailwind-merge was stripping the class, treating it as a duplicate of text-sm.
Reasonable. And wrong.
The DOM told a different story:
"relative flex items-baseline ... transition-colors text-sm ps-6"
ps-6 was there, so the condition evaluated correctly. But text-xs-plus was gone and text-sm had stayed. If tailwind-merge had done its deduplication job, it should have been the other way around.
The check that settles it
Rather than keep guessing, I asked the library directly:
node -e "
const { twMerge } = require('tailwind-merge');
console.log(twMerge('text-sm', 'text-xs-plus'));
console.log(twMerge('text-sm', 'text-2xs'));
"Output:
text-sm text-xs-plus
text-2xs
The second line shows the expected behavior: text-2xs replaces text-sm.
The first strips nothing. It keeps both classes.
The explanation
tailwind-merge has to recognize that two classes belong to the same group before it can arbitrate between them. It knows the standard scale patterns (xs, sm, 2xs, 3xs), but xs-plus looks like nothing it can classify. Unable to identify it as a font size, it lets it through next to text-sm.
So both end up in the DOM, with identical CSS specificity. Stylesheet order decides, and Tailwind sorts its utilities by ascending size. Since text-sm (14px) is generated after text-xs-plus (13px), it wins.
The bug was not in Tailwind, nor in
tailwind-merge. It was in the name I had chosen.
The fix
Rename the token so it follows a recognized pattern:
/* `2sm` (not `xs-plus`): tailwind-merge only treats the
`<n>xs` / `<n>sm` shape as a font size */
--text-2sm: 0.8125rem;
--text-2sm--line-height: 1.5;Verified in the browser this time:
getComputedStyle(h3).fontSize // "13px"
getComputedStyle(h2).fontSize // "14px"What I took away
A name is never neutral
As soon as you use tailwind-merge, so cn(), so any shadcn/ui project, the name of a custom token becomes a technical constraint. It has to stay within the patterns the library can classify.
You think you are naming for humans. You are also naming for a parser.
No tool could have caught it
| Tool | What it checks | Verdict |
|---|---|---|
| TypeScript | Types, not CSS strings | Silent |
| ESLint | Code rules | Silent |
| Tailwind | The token exists, the class is emitted | Silent |
| The build | Compilation succeeds | Silent |
Each one did its job correctly. None had the context: the question was not "does this class exist?" but "does this class win?".
Only the browser settles it
That is the real lesson, and it reaches well beyond Tailwind. A green suite proves the code compiles, not that it does what you think.
For CSS, the check is one line:
getComputedStyle(element).fontSizeThirty seconds. Cheaper than the doubt.
More posts

Startup to Enterprise: what 7 countries taught me
Two years at Decathlon shipping to 7 countries. What stuck with me wasn't the code: it was what breaks when you get it wrong.
7 min read
Next or TanStack? That's not the question
I spent hours comparing frameworks. None of it brought me a single customer. What decides a SaaS is distribution, and it starts before the first line of code.
7 min read