hree years ago I was all-in on utility-first CSS. I had the Tailwind docs bookmarked, I'd written a company-internal talk about it, and I was refactoring every component I touched to strip out .css files. I was, in short, a true believer.
Then I had to debug a button that broke in production. Finding the styles meant reading 47 class names in sequence. I fixed the bug in four minutes and spent twenty minutes afterwards staring at the markup trying to understand what I'd changed.
1The problem isn't the utility classes
Utility classes aren't the problem. The problem is the habit of reaching for them inline for every style decision, including ones that have semantic meaning — hover states, focus rings, theme-specific colours — that belong to a component's contract, not its implementation.
Components should own their visual contract. Inline utilities work for layout and spacing. They fall apart for anything stateful or variant-driven.
— a lesson from six months of regretWhat I've landed on: use Tailwind for layout, spacing, and responsive behaviour. Extract everything else — variants, states, conditional logic — into a typed component interface. The Button should accept variant="primary", not className="bg-[#c94a3a] border-2 border-ink shadow-[0_4px_0_#a23a2c]".
2Co-location without the noise
The pattern that works best for me is a const map of variant strings at the top of the component file. The logic stays in TypeScript where it's type-safe, and the markup stays clean enough to read in a code review.
const variantClasses = {
primary: "bg-barn text-white",
sun: "bg-sun text-ink",
ghost: "bg-white text-ink",
} satisfies Record<Variant, string>~ a note on Tailwind v4 ~
In Tailwind v4, tokens defined in @theme (--color-barn, etc.) become first-class utility classes. You can write bg-barn instead of bg-[#c94a3a]. The variant map approach still applies — the point is that the variant logic lives in TypeScript, not in the JSX.
This is not a revolutionary idea. But it took me three years of Tailwind to stop feeling like extracting variant logic into a map was somehow "less Tailwind" and start treating it as the natural extension of the tool the authors always intended.
3What I'd tell past me
Tailwind is an excellent design token system and a decent layout language. It is not a substitute for component thinking. Use it for the former, delegate the latter to TypeScript, and your future self will thank you at 11pm when something breaks in staging.
- Use Tailwind for layout, spacing, responsive, and animation classes.
- Use variant maps or cva for anything with states or named variants.
- Never put a raw hex value in a className — if it's not a token, make it one.
- Write the types first. The CSS will follow.