I reach for Tailwind first on almost everything now. That is not an aesthetic judgement. The class attributes get long and I am not going to pretend otherwise. It is a judgement about which failure mode I would rather live with.
The alternatives are good, which is the annoying part
This would be an easier argument if the other options were bad.
CSS Modules solved the problem they set out to solve. Scoped class names, real
CSS, no runtime, nothing to learn beyond a naming convention. Vanilla-extract
went further and gave the same thing a type system, so a token is a value your
editor knows about rather than a string you hope still exists. I have shipped
vanilla-extract in production and played with it locally across several projects
since, and I liked it every time. Writing a component’s styles in a .css.ts
file next to it, with autocomplete on your own scale, is a genuinely nice way to
work.
So the case for Tailwind is not that the alternatives are broken. It is that they solve a different problem than the one I keep running into.
The problem is drift, and scoping does not fix it
Scoped styles stop your button bleeding into someone else’s button. They do nothing about the same value being written down in nine places.
I found this in my own codebase, on this site, which has one author and a written design spec. When I audited it:
rounded-[10px]appeared eleven times, while the spec named that exact value--ft-radius-cardtext-[15px]appeared fifteen times, and the spec named it too- twenty-six elements carried an inline
letter-spacing, bypassing the system entirely
One person. A spec sitting in the repo. Still nine places to change if the card radius moves.
CSS Modules would not have caught any of it. Six modules each declaring
border-radius: 10px are six correct-looking files. Nothing is out of scope,
nothing conflicts, nothing warns you. The drift is invisible precisely because
the scoping worked. On a team, with a few people who each own a couple of
components, that is how a design system quietly becomes a suggestion.
Utilities do not fix this by being clever. They fix it by making the wrong thing
visible. You cannot casually write border-radius: 10.5px in a utility
codebase. You have to type rounded-[10.5px], in square brackets, in the markup,
where it looks exactly as much like an exception as it is.
The quirks are real, and I hit five of them on this page
I am not going to sell this as frictionless. Recent, specific, all from building this site on Tailwind v4:
Unknown utilities fail silently. Typo a class and you get no CSS, no error, and a green build. There is no warning because there is nothing to warn about; the utility simply was never generated. The only way I could be sure was to extract all 267 class tokens from source and diff them against the selectors in the compiled stylesheet.
v4 moved translate, rotate and scale onto the individual CSS
properties. They no longer go through the composite transform. That means
transition-transform stops covering a translate-y utility, and, worse, a
prefers-reduced-motion block written as transform: none silently stops
resetting anything. Two hover lifts on this site kept moving for users who had
asked them not to. Reading the CSS would not have told me. I found it by hovering
each element in a headless browser and sampling the computed offset.
The fix for that had its own trap. Adding translate, rotate and scale: none next to transform: none looks right and gets folded by the minifier into
transform: translate(0) rotate(0) scale(1), which sets only the composite
property. The longhands go unreset. The bug comes back, and it is invisible in
the source you wrote.
Named text utilities carry a line-height. --text-3xl is exactly 30px, so
text-[30px] to text-3xl looks like a free cleanup. It is not, because the
named one also sets a line-height and the arbitrary one does not.
Arbitrary values become the default if you let them. The escape hatch is frictionless enough that seventy of mine had exact scale equivalents I had simply not bothered to use.
None of that is a small print footnote. That is a real afternoon.
Why I stay anyway
Because v4 made the token layer the actual source of truth, and that is the part that pays.
Tokens live in @theme. Utilities are generated from them. The design spec and
the stylesheet stop being two documents that agree by convention. When I swapped
this site’s primary accent from green to the blue in my resume, the change was
one hex value and a rename, applied across forty-three references in a single
pass. The page that documents the palette updated itself, because it renders from
the same tokens rather than from a copy of them.
That is the trade. I accept a noisier class attribute and a handful of sharp edges, and I get a codebase where changing a design decision is one edit instead of nine, and where the drift shows up in the markup instead of hiding in a module.
It also is not all-or-nothing, which took me too long to accept. This site has thirty-three component classes for the devices that repeat, the button, the eyebrow, the section strip. Utilities handle everything else. Purity in either direction is the actual mistake. Utilities for the one-offs, a named class the moment something has a name.
Where I would still pick something else
Mostly it comes down to what the team is comfortable with. That is a less satisfying answer than a technical one, and it is the honest one. Styling is a decision everybody on the team touches every day, so the tool that wins is the one people will actually use consistently, not the one that wins the argument in the channel.
Given room to try something, I will take it. shadcn/ui sits on Tailwind and hands you components you own outright instead of a dependency you configure around, which is the right trade for most product work. The one I have been watching is Astryx, Meta’s React component library, which uses StyleX underneath. Accessible and themeable out of the box, with the styling compiled rather than resolved at runtime. If a team had the appetite, I would want to build something real with it.
Where there is no appetite for something new, vanilla-extract or one of its neighbours does the job well and I would not push back.
And if none of that lands, CSS Modules. Not because it is exciting. Because it is safe, everyone already understands it, and shipping matters more than winning the tooling debate.
For an application with a design system behind it, though, which is most of what I build, and where the team is happy with it, I would start on Tailwind again tomorrow. Quirks included. I know what they are now, and I would rather debug a silent utility than find the same hex written down in nine files a year later.