The Tailwind class that never applied
Tailwind generated it, TypeScript validated it, ESLint said nothing. It simply had no effect. A reminder that the only proof lives in the browser.
While adding a custom type scale to this site, I wrote a class that never produced a single pixel of difference. No tool complained. The bug was only visible by inspecting the DOM.
The starting point
Three font sizes were hard-coded across the codebase: text-[11px], text-[10px],
text-[0.8125rem]. The goal was mundane: replace them with design system tokens.
Tailwind 4 is configured directly in CSS, which makes adding them 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 on screen. The table of contents, meant to render its sub-headings at 13px, stubbornly stayed at 14px.
Yet the class was right there in the JSX:
className={cn(
'text-sm transition-colors',
entry.level === 3 && 'text-xs-plus ps-6',
)}What I thought was happening
My first hypothesis was that 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 present, so the condition evaluated correctly. But text-xs-plus was gone, and
text-sm had stayed.
The check that settles it
Rather than keep guessing, a direct test of the library:
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 is the expected behaviour: text-2xs replaces text-sm. The first
strips nothing at all: it keeps both classes.
The real explanation
tailwind-merge needs to know that two classes belong to the same group in order to
arbitrate between them. It recognises the standard scale patterns (xs, sm, 2xs,
3xs), but xs-plus looks like nothing it knows. Unable to identify it as a font size, it
lets it through alongside text-sm.
So both classes end up in the DOM, with identical CSS specificity. Stylesheet order decides
the winner, and Tailwind sorts its utilities by ascending size. Since text-sm (14px) is
generated after text-xs-plus (13px), it wins.
The bug wasn't in Tailwind, nor in
tailwind-merge. It was in the name I had chosen.
The fix
Rename the token so it follows a recognised 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
Naming a token isn't free
As soon as you use tailwind-merge, so as soon as you use cn(), so on 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.
It's counterintuitive: you think you're naming for humans, when you're 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 necessary context: the question wasn't "does this class exist?" but "does this class win?".
The proof is in the browser
That's the real lesson, and it reaches well beyond Tailwind. A green suite proves the code compiles, not that it produces the intended result. For CSS, the check is one line:
getComputedStyle(element).fontSizeThirty seconds. That's the price of certainty, and it's far below the price of doubt.
More posts

Startup to Enterprise: what 5 countries taught me
After 5 years coding from startups to enterprise, scaling taught me one thing: the real nightmare isn't the code, it's the blast radius.
5 min read
Next or TanStack? That's not the question
I use both. My portfolio runs on Next, my SaaS on TanStack, and both are in production. What matters is what ships.
4 min read