Tailwind CSS: Why I Switched to the Utility-First Approach
Why I adopted the utility-first CSS paradigm with Tailwind CSS v1.0, the trade-offs I encountered, and when it actually makes sense.
Tailwind CSS v1.0 was released in May 2019. I had tried the beta beforehand, but with the stable release I started using it in a real project. Before sharing the outcome, let me be upfront: I pushed back at first. Embedding long class lists directly into HTML felt wrong to me. But when I set aside my bias and actually tried it, my opinion changed.
Why utility-first?
BEM (Block Element Modifier), SMACSS, component-based CSS — I’ve used all of them. The core problem with every one of those approaches was the same: when writing CSS for a component, you’re constantly jumping between two files. You invent a class name, hunt down where it’s defined, then figure out what else breaks when you change it. As CSS grows, that overhead compounds.
In the utility-first approach, every class represents a single CSS rule: mt-4 means four units of top margin, text-gray-700 is a specific shade of gray, flex sets flexible layout. You don’t invent a custom name for a component — you compose existing utility classes.
<!-- BEM approach -->
<button class="button button--primary button--large">Submit</button>
<!-- Tailwind approach -->
<button class="px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700">
Submit
</button>
When you first see the second version, your instinct might be “unreadable.” Mine was too. But after a while I noticed something: reading that one HTML line tells me exactly how the button looks — no separate CSS file needed.
Real advantages
CSS doesn’t grow. With a traditional approach, your CSS file expands every time you add a feature. With Tailwind, no matter how many features you add, the classes you use come from a fixed pool. Once you set up a pipeline that strips unused classes in production with PurgeCSS, the final CSS file can be just a few kilobytes.
Design consistency emerges naturally. Spacing, colors, font sizes — you use these through the scale defined in tailwind.config.js, not arbitrary values. One developer writes mt-4; another won’t write mt-[17px] by accident, and if they do, it stands out as an exception.
Prototyping speed. Tailwind is remarkably efficient when you need to put together a UI quickly. The write-CSS → save → switch-to-browser cycle disappears entirely.
Not ignoring the trade-offs
HTML gets noisy. That’s real. Class lists grow long, especially once you add breakpoint prefixes for responsive design (md:flex lg:grid). Tailwind’s @apply directive lets you extract repeated patterns into proper classes, but if you overuse it the question “why did we choose Tailwind?” starts coming back.
Initial learning curve. You need to either memorize Tailwind’s class names or keep the docs open constantly. The first couple of weeks feel slow.
Without a design system, things get messy. Tailwind is a great tool, but it’s still possible to use it badly. If you start without properly defining your color palette, spacing scale, and typography in tailwind.config.js, you’ll end up with inline overrides scattered everywhere.
When I reach for it — and when I don’t
If I’m running a project alone or with a small team and the design system isn’t mature yet, Tailwind is highly productive. It fits well in projects where I like working close to the HTML layer — Laravel Blade templates and Vue components in particular.
If there’s a large front-end team and the design system is built on top of a component library, the utility-first approach can create friction. Component-based CSS is a better fit there.
It would have been easy to write Tailwind off as just another hyped tool. But you have to actually use it to understand the idea behind it. For me, in the right context, it’s genuinely efficient — in other contexts, it isn’t.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.