I was a CSS purist for the better part of a decade. Semantic class names. BEM methodology. Carefully organized stylesheets split by component, page, and concern. I wrote Sass mixins I was genuinely proud of. When I first saw Tailwind's markup — a div wearing twenty utility classes like some kind of inline-style fever dream — I dismissed it immediately.
Then I joined a team that used it on a production app with 200+ components. Within two weeks, I stopped fighting it. Within a month, I was evangelizing it. Here is the honest story of what changed my mind, what I still miss, and when I think you should absolutely not use Tailwind.
The Maintenance Problem Nobody Talks About
Every CSS methodology promises organization. BEM gives you naming conventions. CSS Modules give you scoping. Styled-components give you colocation. They all work beautifully on day one. The problem reveals itself at month six.
Here is what a typical component looks like with traditional CSS early in a project:
/* Card.css */
.card {
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
background: white;
}
.card__title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.card__body {
color: #6b7280;
line-height: 1.6;
}
Clean. Readable. Now multiply this by 200 components across a team of five developers over eighteen months. Here is what actually happens:
- Dead CSS accumulates. Someone refactors
CardtoContentCardbut the oldcard.cssfile lingers because nobody is sure if something else imports it. - Naming becomes political. Is it
.card__footer-linkor.card__footer .link? Every team invents its own micro-dialect of BEM. - Specificity creeps in. Someone nests a
.cardinside a.sidebarand suddenly needs.sidebar .card__titleto override a font size. Three months later,!importantappears. - Context switching kills velocity. You are reading JSX, see
className="card__title", and need to open a separate file, find the right selector, and mentally map it back.
I kept telling myself this was a skill issue. But discipline does not scale across teams. Systems that require discipline to maintain correctly are systems waiting to fail.
Colocation: The Real Insight
The single biggest advantage of Tailwind is not the utility classes themselves. It is the colocation of style and structure:
function Card({ title, body }: CardProps) {
return (
<div className="p-6 border border-gray-200 rounded-lg bg-white
hover:shadow-md transition-shadow">
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<p className="text-gray-500 leading-relaxed">{body}</p>
</div>
);
}
Delete the component, and its styles vanish. No orphaned CSS. No grep-and-pray cleanup sessions. The garbage collector for your styles is the file system.
Code review becomes meaningful. When someone changes a component's appearance, the diff shows exactly what changed and where. No cross-referencing between JSX and CSS diffs.
Refactoring is fearless. Want to split Card into CompactCard and ExpandedCard? Copy the JSX, adjust the classes. No risk of breaking a selector chain.
Critics say this trades separation of concerns for coupling. But in component-based architecture, the component is the concern. A React component that owns its markup, logic, and styles is expressing a coherent unit of UI.
Design System Constraints That Actually Work
Here is something that surprised me: Tailwind made our UI more consistent, not less.
With freeform CSS, spacing is a creative decision every time:
.header { padding: 18px 24px; }
.sidebar { padding: 1.1rem 1.4rem; }
.content { padding: 20px; }
With Tailwind, you choose from a fixed scale: p-4 (1rem), p-5 (1.25rem), p-6 (1.5rem). You cannot accidentally use padding: 17px because there is no class for it. The framework nudges every developer toward the same set of values.
You can customize the scale to match your brand's design tokens:
module.exports = {
theme: {
extend: {
colors: {
brand: {
50: '#f0f7ff',
500: '#3b82f6',
900: '#1e3a5f',
},
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
},
},
};
Now bg-brand-500 and p-18 are available everywhere, and they mean the same thing to every developer on the team. That is a design system that enforces itself.
Responsive Design Without the Mental Gymnastics
Responsive design in traditional CSS means jumping between the component and a separate media query block:
.dashboard-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
}
@media (min-width: 1024px) {
.dashboard-grid {
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
}
The Tailwind equivalent keeps all breakpoint information inline:
<div className="grid grid-cols-1 gap-4
md:grid-cols-2 md:gap-6
lg:grid-cols-3 lg:gap-8">
{items.map(item => <DashboardCard key={item.id} {...item} />)}
</div>
The entire responsive behavior is in one place. The mobile-first progression reads left to right: base, then md:, then lg:.
Dark Mode for Free
With Tailwind, dark mode is a prefix — no separate block, no separate file:
<div className="bg-white text-gray-900 border-gray-200
dark:bg-gray-800 dark:text-gray-100 dark:border-gray-700">
{/* content */}
</div>
Every element declares its own light and dark appearance right where it is defined. Dark mode support becomes part of the initial implementation, not a follow-up ticket that sits in the backlog for months.
Performance: Ship Only What You Use
Tailwind scans your source files and generates only the utility classes you actually use. A typical production build generates 8-15 KB of CSS (gzipped). Compare that to Bootstrap's 25+ KB or a mature custom stylesheet that has grown to 50-100 KB.
This is not a bolt-on optimization. Unused styles were never generated in the first place.
When NOT to Use Tailwind
Honesty matters more than advocacy:
Complex animations and transitions. Anything beyond simple hover states gets unwieldy in utility classes. A multi-step keyframe animation belongs in a CSS file:
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton-loader {
background: linear-gradient(
90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
Pseudo-element art and creative CSS. Gradient text effects, CSS-only illustrations, decorative ::before and ::after elements — this is where CSS shines as a creative medium.
Highly dynamic styles. When values come from runtime data — a user-chosen color, a computed position — you need inline styles or CSS custom properties.
The Best of Both Worlds
The teams I have seen succeed treat Tailwind and custom CSS as complementary:
function HeroSection() {
return (
<section className="relative min-h-screen flex items-center
justify-center px-6 overflow-hidden">
<div className="relative z-10 text-center max-w-3xl">
<h1 className="text-5xl md:text-7xl font-bold mb-6">
<span className="gradient-text">Build Something</span>
{' '}Beautiful
</h1>
</div>
<div className="animated-gradient-bg absolute inset-0" />
</section>
);
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.animated-gradient-bg {
background: linear-gradient(
-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab
);
background-size: 400% 400%;
animation: gradient-shift 15s ease infinite;
opacity: 0.1;
}
Tailwind for 90% of your styles. CSS for the 10% that needs creative freedom. The custom CSS file stays small because it only contains styles that cannot be expressed as utilities.
The Honest Tradeoffs
- The learning curve is real. Budget a week of slower output for new developers to internalize the naming conventions.
- Long class strings are noisy. Editor tooling (Tailwind IntelliSense, Prettier's tailwind plugin) helps, but the visual noise is undeniable.
- Debugging in DevTools is different. You are editing individual utility classes, not a named selector.
- It is an abstraction, and abstractions leak. Sometimes you will fight Tailwind's opinion. When that happens, reach for custom CSS without guilt.
The Bottom Line
I did not switch to Tailwind because it is theoretically superior. I switched because it solved the problems I actually had: dead code accumulation, inconsistent spacing, painful responsive workflows, and constant context-switching.
The lesson is not "Tailwind is better than CSS." CSS is the foundation — Tailwind is a pragmatic interface to it. If you are optimizing for maintainability at scale, developer velocity, and design consistency across a team, Tailwind earns its place in your stack.
If you are creating CSS art or building a highly custom visual experience, write CSS directly and enjoy every line of it. The best frontend developers I know are fluent in both — and they choose the right tool without dogma.