Quick disclosure from Sal (@dested): we were going to build and release this. I had the idea, and Claude Fable 5 did the work — the code, the measurements, and this writeup. I’m publishing its lab notes as-is because pretending otherwise would be weird. The rest is the AI talking.
The plan
LLMs are great at React largely because of Tailwind — style and structure in one file. But Tailwind is verbose, and LLM output is priced and paced per token. So we were going to release Terse Tailwind: a mapping from every class to a short alias — items-center → ic, justify-between → jb, dark:hover:bg-gray-800 → d:h:bg8 — shipped as a skill, so the model emits the cheap vocabulary and a build step expands it back.
Before writing the plugin, we measured. That’s where it died. The flaw hides in the word “verbose” — verbose in what unit?
Method
- Built a complex dashboard in standard Tailwind — sidebar, topbar, stat cards, chart, activity feed, data table, responsive + dark mode + hover/focus. 143
classNamestrings. - Designed a 183-class alias map and a ~40-line codemod to apply it.
- Rebuilt the same page two more ways: pure CSS, and componentized React (data arrays +
.map(), still standard Tailwind). - Counted tokens with
tiktoken(o200k_base). - Rendered every version in headless Chrome to prove they’re the same page.
Finding 1: the tokenizer got there first
LLMs don’t read characters — they read tokens, chunks produced by byte-pair encoding (BPE). BPE builds its vocabulary by repeatedly merging the character pairs most frequent in training data, so common strings collapse into single tokens no matter how long they look. Tailwind classes are everywhere in training data. They’re already compressed:
"flex items-center justify-between" 33 chars → 5 tokens
"fx ic jb" 8 chars → 3 tokens
| standard | terse | savings | |
|---|---|---|---|
| className characters | 9,237 | 3,202 | 65% |
| className tokens | 2,537 | 1,749 | 31% |
| whole-file tokens | 4,496 | 3,708 | 17.5% |
The scheme promises 65%. It delivers 17.5%, with a hard floor: a class can never cost less than one token, and common Tailwind classes already average ~1.7. Any abbreviation scheme caps out around 40% of class tokens; mine is already near it.
And this page is best-case — markup-only, 56% of its tokens in class strings. Real components carry imports, state, and handlers, so realistic savings land around 8–11%. The price: a model pushed off the distribution where its Tailwind fluency lives, broken copy-paste from every doc, and a Tailwind plugin required to compile at all. One hallucinated alias erases the savings on a lot of files.
Finding 2: pure CSS is worse. Much worse.
Same page, semantic classes, hand-written stylesheet with CSS variables for theming:
| approach | tokens | vs standard Tailwind |
|---|---|---|
| pure CSS: JSX alone | 2,186 | 51% smaller |
| pure CSS: stylesheet alone | 4,296 | |
| pure CSS total | 6,482 | 44% LARGER |
The JSX alone is the cheapest file in the experiment — but generation and every style edit need both files, and then you pay 44% more. display: flex; align-items: center; costs far more than flex items-center, selectors repeat class names, media queries repeat them again, and every styling change is a two-file sync problem.
I stacked the deck for CSS — this page is full of repeated components sharing rules — and it still lost. Tailwind already is the terse encoding of CSS. BPE compresses it a second time. The alias idea was compressing something that’s been compressed twice.
Finding 3: the win was in the repeated markup
The test page was written the way LLMs write one-shot pages: four copy-pasted stat cards, three copy-pasted table rows. The boring fix — data arrays, .map(), a shared Badge and Panel:
| approach | tokens | vs standard |
|---|---|---|
| standard Tailwind | 4,496 | — |
| terse Tailwind | 3,708 | −17.5% |
| componentized Tailwind | 3,206 | −28.7% |
| componentized + terse | 2,766 | −38.5% |
| pure CSS (both files) | 6,482 | +44.2% |
Componentization beats the abbreviation scheme and costs nothing: on-distribution, no alias map, no broken tooling, and it’s what code review would demand anyway.
Abbreviation compresses syntax; abstraction compresses semantics — and semantics compress harder. tg5 saves a token. {STATS.map(...)} saves a stat card. <StatCard label="Revenue" value="$48,210" delta="+12.3%" /> is the tersest possible encoding of a stat card — which is a decent theory of why shadcn-style component libraries won LLM codegen. The ultimate compression is a function call.
TL;DR
- Terse Tailwind: 65% fewer characters, 17.5% fewer tokens, ~10% in real files. BPE beat you to it, and there’s a 1-token-per-class floor you can’t tunnel under.
- Pure CSS: 44% more tokens. Tailwind already is the terse encoding of CSS.
- Componentization: 28.7% fewer tokens. Free, on-distribution, better code.
- Prompt caching handles input. Diff edits handle rewrites. Components handle output. Vocabulary was never the lever.
Caveat: this measures encoding, not generation quality — and quality is where terse would lose again, since aliases forfeit the training prior that makes models good at Tailwind.
Sal got his weekend back. He’s @dested on Twitter.
Code, alias map, counting scripts: dested/terse-tailwind.