🌬️ My Love-Hate Relationship with Tailwind CSS Why I reach for it more often than not, despite the taste it leaves in my mouth

Feb 27, 2025

“Just add a few utility classes,” they said. “It’ll be faster,” they promised. As someone who spent years crafting meticulously organized CSS architectures, these words initially felt like nails on a chalkboard. I was that developer who started his CSS journey insisting on BEM methodology, maintained strict separation of concerns, and viewed inline styles as a cardinal sin. Eventually I moved on to CSS-in-JS solutions because of the tight scoping and clean DX. Yet here I am, five years later, reaching for Tailwind CSS on almost every new project.

This isn’t a story of complete conversion—it’s a pragmatic journey from skepticism to acceptance. With Tailwind CSS 4.0 now available, it feels like the right time to dissect this cognitive dissonance that many developers, including myself, experience: loving a tool that seemingly violates everything we were taught about “proper” CSS.

Let me show you why I’ve changed my stance, what still makes me wince, and how Tailwind’s latest version addresses some of my longest-standing objections.

My Remaining Objections

Utility Class Proliferation

The fundamental issue I’ve had with Tailwind is the sheer volume of utility classes that end up in your HTML. Instead of writing:

HTML

<button class="primary-button">Click Me</button>

with clean, semantic CSS elsewhere, you’re writing:

HTML

<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Click Me
</button>

Initially, this approach felt wrong. It seemed to violate separation of concerns and made my markup look busy. But I’ve come to understand that this “violation” is actually Tailwind’s superpower.

This realization came as I reflected on my move to CSS-in-JS libraries such as the popular styled-components package. Styled-components also felt foreign to me at first, but it grew on me rather quickly. I realize now that was because most of the time, I was writing components with tightly coupled styles anyway. The styles weren’t reused across different components, and the “separation” was more about file organization than actual code reuse. Tailwind simply makes this coupling more explicit, while providing a consistent design system that styled-components doesn’t offer out of the box.

The traditional separation between HTML and CSS creates an artificial boundary. When we write class="primary-button", we’re not actually separating concerns—we’re just hiding them. That CSS has to live somewhere, and changing it requires context-switching between files, understanding the cascade, and managing specificity.

Utility classes flip this model on its head. Instead of maintaining parallel structures of HTML and CSS that must stay in sync, Tailwind embraces a component-oriented model where styles live directly with their markup. This brings several key benefits:

  1. Locality of Behavior: Every aspect of how an element looks and behaves is visible right there in the markup. No hunting through stylesheets, no guessing at specificity, no unexpected cascade effects.

  2. Direct Manipulation: When you need to adjust a padding or change a color, you do it right where you see it. This immediate feedback loop is why Tailwind feels so productive in practice.

  3. Self-Documenting: Utility classes serve as inline documentation. Instead of wondering what primary-button includes, you can see exactly what styles are being applied.

  4. Constraint-Based Design: Tailwind’s utility classes aren’t just shortcuts—they’re a design system in disguise. Using p-4 instead of padding: 17px naturally encourages consistency across your UI.

Yes, the markup is more verbose. But this verbosity is a feature, not a bug—it makes our styling intentions explicit and our components more maintainable. The “separation of concerns” we were taught to value was actually creating more problems than it solved.

This realization was key to my journey from skeptic to advocate. Sometimes more verbose code is more maintainable code, and sometimes what looks messier on the surface is actually cleaner under the hood.

Bundle Size and Code Splitting Challenges

Bundle size remains a significant concern, albeit a nuanced one. While Tailwind’s purge process effectively eliminates unused styles in production, the relationship between utility classes and modern code splitting deserves closer examination.

This is particularly relevant in content-heavy sites like this Astro blog. Astro’s partial hydration model means each page should only ship the minimal JavaScript needed, but Tailwind’s utility classes don’t naturally align with this paradigm. Even though I’m writing this article in MDX with minimal interactivity, the full set of Tailwind styles for elements like code blocks, blockquotes, and interactive components must be included in the page bundle. Astro’s excellent static site generation capabilities can’t fully optimize these styles on a per-page basis the way it can with JavaScript.

For instance, this article might only use a fraction of Tailwind’s typography utilities, but the build process can’t easily determine which styles might be needed by the MDX content at runtime. This means either shipping more CSS than necessary or implementing complex build-time analysis to extract only the used classes from the markdown content.

The challenge isn’t just about final bundle size—Tailwind with proper purging can actually result in smaller CSS than traditional approaches. The real issue lies in the granularity of code splitting. When styles are embedded directly in markup, we lose the ability to split CSS independently from HTML and JavaScript. This means that even with route-based code splitting, each chunk must include its complete set of styles, potentially leading to style duplication across chunks.

This becomes particularly relevant in larger applications where different teams work on separate features, or in micro-frontend architectures where independent deployability is crucial. While tools like @layer and @apply can help mitigate these issues, they somewhat defeat the purpose of Tailwind’s utility-first approach.

However, it’s worth noting that for many applications, especially those under 50k lines of code, these concerns are more theoretical than practical. The benefits of Tailwind’s development experience often outweigh the marginal performance impact of suboptimal code splitting.

Why I’ve Come Around (Somewhat)

Speed of Development

The development velocity with Tailwind is undeniable. Instead of this common workflow:

  1. Write HTML
  2. Switch to CSS file
  3. Create new class
  4. Return to HTML to add class
  5. Adjust CSS values repeatedly
  6. Deal with specificity conflicts

You simply iterate directly in your markup. Need to adjust spacing? Change p-4 to p-6. Want a different shade of blue? bg-blue-500 becomes bg-blue-600. This immediate feedback loop transforms the development experience from context-switching chaos to fluid iteration.

Consistency Without Effort

Tailwind’s constraint-based design system is its hidden superpower. Instead of:

.custom-component {
  padding: 13px;
  margin-bottom: 27px;
  font-size: 15.5px;
}

You’re working with a predictable scale: p-4, mb-8, text-base. This built-in constraint system:

  • Enforces visual rhythm across your entire application
  • Prevents “pixel-pushing” perfectionism
  • Makes responsive design more systematic
  • Enables team-wide consistency without endless documentation

The Documentation Is Exceptional

Tailwind’s docs aren’t just comprehensive—they’re a masterclass in developer experience. Every utility includes:

  • Interactive examples
  • Common use-cases
  • Browser compatibility notes
  • Performance considerations The searchable interface and logical organization mean you spend less time searching Stack Overflow and more time building. Compare this to wrestling with CSS-in-JS documentation spread across multiple packages and versions.

It Just Works

After spending countless hours debugging CSS-in-JS setup issues:

  • Vanilla Extract’s compilation breaking in Next.js
  • Linaria’s SSR hydration mismatches
  • Styled Components requiring special Babel configs
  • CSS Modules needing custom TypeScript settings

Tailwind is refreshingly simple, even more so in the new v4 release. Install the package, add the import to your global stylesheet, and you’re running. No runtime overhead, no build complications, no SSR headaches. In a world where frontend tooling complexity keeps increasing, this simplicity is invaluable.

Tailwind v4: A Significant Evolution

The latest version of Tailwind represents more than just incremental improvements—it’s a fundamental evolution that addresses several core pain points in modern web development.

Lightning Fast Development Experience

V4’s architecture leverages TypeScript for its core functionality while strategically incorporating Rust-based tools like LightningCSS for performance-critical operations. This hybrid approach delivers:

  • Near-instantaneous HMR updates
  • Lightning-fast CSS parsing and transformation
  • Improved type safety and developer tooling
  • Significantly reduced CI/CD build times

The performance gains come not just from the partial use of Rust, but from architectural improvements like:

  • Smarter caching strategies
  • Optimized parsing algorithms
  • Better integration with build tools
  • More efficient CSS generation

Container Queries: A Game-Changer for Component Design

One of v4’s most significant additions is native support for container queries, fundamentally changing how we build responsive components. Instead of relying solely on viewport-based media queries, we can now create truly reusable components that adapt to their available space.

The syntax is refreshingly simple:

<div class="@container">
  <div class="flex flex-col @md:flex-row">
    <!-- Component adapts based on container width, not viewport -->
  </div>
</div>

This mobile-first approach follows the same intuitive patterns as Tailwind’s viewport breakpoints, but with three key advantages:

  1. Component-Level Responsiveness: Components respond to their immediate context rather than the viewport size. A sidebar widget can adapt its layout whether it’s in a narrow sidebar or a wide main content area.

  2. Predictable Behavior: Since components respond to their container’s size, you can be confident they’ll look right regardless of where they’re placed in your application.

  3. Better Component Reusability: The same component can adapt appropriately whether it’s used in a card, modal, or full-width section.

For more complex layouts, v4 provides several powerful container query features:

<!-- Target specific size ranges -->
<div class="@container">
  <div class="@sm:@max-lg:grid-cols-2">
    <!-- Two columns only between small and large container sizes -->
  </div>
</div>

<!-- Name containers for precise targeting -->
<div class="@container/sidebar">
  <div class="@lg/sidebar:hidden">
    <!-- Responds to sidebar width specifically -->
  </div>
</div>

<!-- Use container-relative units -->
<div class="@container">
  <div class="w-[50cqw]">
    <!-- Width relative to container, not viewport -->
  </div>
</div>

This feature set transforms how we think about responsive design. Instead of building page-level responsiveness with media queries, we can create truly modular components that intelligently adapt to their context. Combined with Tailwind’s utility-first approach, container queries make it easier than ever to build flexible, maintainable component libraries.

Enhanced Color System

The color system overhaul isn’t just about new colors—it’s about better design decisions:

  • WCAG 2.1 compliance built into default color relationships
  • Improved contrast ratios across the palette
  • Semantic color naming that makes accessibility easier
  • Better support for dark mode transitions

CSS-First Configuration: A Welcome Simplification

One of v4’s most transformative changes is the shift from JavaScript-based configuration to CSS-based configuration. Gone is the tailwind.config.js file, replaced by a more intuitive, CSS-native approach:

@import "tailwindcss";
@theme {
  --font-display: "Inter", sans-serif;
  --color-primary-500: oklch(0.84 0.18 117.33);
  --spacing-xl: 2.5rem;
  --ease-bounce: cubic-bezier(0.2, 0, 0.13, 1.5);
}

This shift brings several significant advantages:

  1. Native CSS Variables: All theme values are automatically exposed as CSS custom properties, making them accessible anywhere in your styles without JavaScript:

    <div style="padding: var(--spacing-xl)">
      <!-- Access theme values directly -->
    </div>
  2. Simplified Setup: One less configuration file to manage, and all your styling decisions live where they belong—in your CSS.

  3. Better Developer Experience: Real-time feedback in your CSS editor, including syntax highlighting and autocompletion for CSS custom properties.

  4. Runtime Flexibility: Theme values can be modified dynamically without rebuilding, perfect for features like theme switching or user customization.

The system is also more powerful than before. You can define complex values that would have been awkward in JavaScript:

@theme {
  /* Complex gradients */
  --gradient-hero: linear-gradient(
    73deg,
    oklch(0.92 0.19 114.08) 0%,
    oklch(0.84 0.18 117.33) 100%
  );
  
  /* Animation curves */
  --ease-elastic: cubic-bezier(0.2, 0, 0.13, 1.5);
  
  /* Complex shadows */
  --shadow-elevated: 
    0px 2px 4px -2px rgb(0 0 0 / 0.05),
    0px 4px 8px -2px rgb(0 0 0 / 0.1);
}

This CSS-first approach represents a significant step forward in Tailwind’s evolution, making the framework feel more native to the web platform while reducing configuration complexity. It’s a change that aligns perfectly with modern CSS practices and makes the framework more accessible to developers who are more comfortable with CSS than JavaScript.

These improvements don’t just solve technical problems—they enable better design patterns and development workflows. V4 shows that Tailwind isn’t just keeping up with modern CSS; it’s helping to shape how we think about web styling.

The Pragmatic Conclusion

While I still find the Tailwind approach somewhat distasteful from a purist perspective, I’ve had to come to terms with an undeniable truth: it solves real problems. The practical benefits—rapid development, consistent design, reduced CSS complexity—aren’t just marketing points; they’re tangible advantages I experience daily.

The developer experience—especially with the v4 improvements—has reached a level where ignoring it feels professionally irresponsible. When I can prototype features in half the time, when teams can maintain consistent design language without extensive documentation, and when I spend less time fighting specificity wars, the philosophical debates about “separation of concerns” start to feel academic.

Am I completely converted? No. There are specific scenarios where traditional CSS approaches still make more sense:

  • Design systems requiring deep CSS customization
  • Performance-critical applications needing fine-grained code splitting
  • Applications with complex animation requirements

But for the majority of web projects—from marketing sites to web applications to content-heavy platforms like this blog—Tailwind has earned its place as my default choice. It’s not about abandoning CSS principles; it’s about acknowledging that those principles serve developers, not the other way around.

Sometimes the pragmatic solution wins not because it’s theoretically perfect, but because it makes developers more productive and projects more maintainable. Tailwind CSS has achieved this balance, even if it means writing HTML that would make a CSS purist wince. And I’m okay with that.