Tailwind: the best and worst thing to happen to front-end dev
I have a confession: I think Tailwind is both the worst and the best thing to happen to front-end development, and I use it anyway.
Every time I open devtools on a Tailwind project, a small part of me dies. Every time I ship a component in half the time it used to take, that part comes back to life. This post is me trying to make peace with both feelings.
The worst: good luck finding anything
Here’s a real-ish line from a production codebase:
<div class="flex items-center justify-between gap-4 rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 md:px-6">
What is this? A card? A list row? A nav item? A modal header? You have no idea, and neither does anyone else who opens the inspector. Old-school CSS gave you .product-card or .checkout-summary. The class name told you what the thing was. Tailwind only tells you what the thing looks like.
That sounds like a small loss until you actually live with it:
- Devtools becomes a guessing game. You click into a wall of nested
divs, all wearing the same twelve utilities, and try to match what you see on screen to the code in your editor. - Searching the codebase is useless. Grepping for
flex items-centerreturns half the project. There’s no unique handle to search for. - Testing and automation get harder. Selectors for end-to-end tests, analytics tags, and browser extensions used to hang off meaningful class names. Now you’re sprinkling
data-testideverywhere to bring back the identifiers you lost. - The markup is just ugly. Long class strings wrap across multiple lines, diffs get noisy, and reviewing a PR means reading CSS backwards, one abbreviation at a time.
Tailwind’s answer is “the component is the name.” In your editor that’s mostly true: you see <ProductCard>. But the browser never sees your component names. It sees the soup.
The best: it’s just so fast
And yet. I can’t deny it: Tailwind makes styling faster and simpler in a lot of ways.
- No more naming things. Naming is one of the two hard problems in computer science, and Tailwind deletes it from your day. No debating
.card__header--activeversus.card-header.is-active. - No context switching. You style the element where it lives. No jumping between a component file and a stylesheet, no hunting for which rule is winning.
- No dead CSS. Delete a component and its styles go with it. Nobody is afraid to touch a global stylesheet because nobody has one.
- Built-in constraints. Spacing, colors, and type sizes come from a scale.
p-4is alwaysp-4. A team of ten ends up with a far more consistent UI than ten people writing freehand pixel values. - Responsive and state styles are trivial. Things that used to need a media query block or a separate selector are now a prefix away:
md:,hover:,dark:,focus-visible:. - Specificity wars are over. Everything is one class deep. No more
!importantarms races.
The result is that you stop thinking about CSS architecture and just build the thing. For most product work, that’s exactly the right trade.
Why it won: React never had a real styling story
Honestly, a lot of Tailwind’s success is React’s fault. React told us how to build components and then shrugged when it came to styling them. Every option was a compromise:
- Inline
style={{}}objects: camelCased CSS in JavaScript, no hover states, no media queries, no pseudo-elements. - Global CSS files: completely disconnected from the components that use them, and one bad selector away from breaking something on another page.
- CSS Modules: better scoping, but you’re back to naming everything and juggling a second file per component.
- CSS-in-JS (styled-components, Emotion): nice ergonomics, but runtime cost, bigger bundles, and an awkward fit with server components.
Tailwind slotted into that gap perfectly. It’s colocated like inline styles, but it supports every state and breakpoint. It’s scoped by default because there’s nothing global to collide with. And it compiles to a small static stylesheet, so it plays nicely with server rendering.
In a world where the component is already the unit of reuse, utility classes stop looking crazy. They look like the styling model React should have shipped with.
Which is a little ironic, because Svelte and Vue already solved this. Drop a <style> block into a .svelte file, or a <style scoped> block into a Vue single-file component, and your CSS is automatically scoped to that component. You get real CSS, real selectors, meaningful class names, and colocation, with no utility soup required. A lot of what makes Tailwind feel like a revelation in React is just catching up to what those frameworks shipped years ago.
Like it or not, it’s the convention now
Even if you’d rather write plain CSS, opting out of Tailwind is getting harder. A big chunk of the UI ecosystem now assumes it:
- shadcn/ui hands you components as source code styled entirely with Tailwind classes.
- Headless UI, Flowbite, daisyUI and plenty of others are built for or on top of it.
- Templates, starter kits, and docs examples increasingly ship Tailwind markup by default.
- AI coding tools tend to spit out Tailwind classes because that’s what most of the code they learned from looks like.
So the choice isn’t really “Tailwind or not” anymore. If you want to copy a component, use a popular kit, or read most modern examples, you’re going to be reading and writing utility classes. Tailwind won partly on merit and partly on momentum, and at this point the momentum is self-sustaining.
Living with it: getting the speed without the mess
You don’t have to pick a side. A few habits fix most of the pain:
- Add one semantic hook per meaningful element. A plain class like
product-cardor adata-component="ProductCard"attribute next to the utilities costs nothing and makes devtools readable again. Tailwind doesn’t care if an unrelated class is sitting there. - Extract components early. If the same class string shows up three times, it’s a component, not a copy-paste job. This is where Tailwind’s “the component is the name” argument actually holds up.
- Use
@applysparingly, but use it. For truly global bits (base typography, form resets, prose content you don’t control), a small stylesheet is still the right tool. - Let a formatter sort your classes. The official Prettier plugin orders utilities consistently, which makes long strings scannable and diffs much quieter.
- Group variants with helpers. Libraries like
clsx,cvaortailwind-mergelet you name variants (size: "sm",intent: "danger") instead of inlining conditional class soup. - Install the editor extension. Hover previews and autocomplete turn the cryptic abbreviations back into real CSS when you need to know what’s going on.
The verdict
Tailwind trades readability in the browser for speed in the editor. For most teams building in React, that’s a good deal, which is why it’s everywhere.
But it’s still a trade. The markup is a mess, the inspector is a maze, and the semantic meaning that CSS class names used to carry is gone unless you put it back yourself. So use Tailwind, enjoy shipping faster, and then do future-you a favour: leave a few real names behind for whoever has to debug it at 2 a.m.