~ pardon the mess ~

Our farm is under construction.

We're rebuilding fences, painting barns, and teaching the sheep new tricks. Some pages may be a bit dusty — check back next week for the full opening.

Arun Negi
✉ Get in touch
WritingEngineeringThis story
Engineering

Why I stopped writing utility classes inline

A practical case for co-locating styles with logic, and what I learned after three years of Tailwind.

AN
Arun Negi@aruns-farm
April 14, 20256 min read

~ from the desk of Arun ~

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 regret

What 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.
Share →
~ keep reading ~

More Writing

All stories →