Tailwind CSS did not become one of the most downloaded npm packages by accident. It solved a real, painful problem: keeping CSS from turning into an unmaintainable pile of specificity conflicts and dead rules. But a tool that solves one problem can quietly introduce others — and the growing conversation around moving away from utility-first CSS is worth taking seriously.

This is not a Tailwind takedown. It is a practical look at the trade-offs, and a guide for structuring CSS when you decide the utility-first model is no longer the right fit for your project.


Why Utility-First CSS Became the Default

Before Tailwind, most teams bounced between two failure modes:

  • Giant global stylesheets where every new rule risked breaking something else
  • CSS-in-JS libraries that tied styling tightly to framework-specific runtime behaviour

Tailwind's utility classes (flex, mt-4, text-sm) offered a third path: co-locate styles with markup, keep the stylesheet tiny via PurgeCSS, and never name another class again. For rapid prototyping and design-system-driven UIs, that trade-off is excellent.

The problems tend to surface later.


The Real Costs That Accumulate Over Time

Markup Becomes Difficult to Scan

A single button with a hover state, responsive variants, and a dark-mode override can produce a class attribute that is 200+ characters long. That is not a hypothetical — it is what production Tailwind components routinely look like. Reviewing such markup in a pull request is genuinely harder than reading a semantic class name backed by a short CSS rule.

Abstraction Leaks Into the Wrong Layer

HTML is supposed to describe what content is; CSS is supposed to describe how it looks. Utility-first CSS collapses those layers. In small projects that is a reasonable trade-off. In large codebases with multiple contributors, it means design decisions are scattered across hundreds of JSX files rather than centralised in a style layer that designers and developers can reason about independently.

Custom Design Tokens Get Awkward Fast

Tailwind's configuration file is powerful, but every project-specific value — a brand colour, a non-standard spacing step, a custom animation — requires a round-trip through tailwind.config.js. After a few months, that file becomes its own maintenance burden, and you end up with arbitrary value escapes like w-[327px] that defeat the purpose of a constraint-based system.


What Well-Structured Vanilla CSS Actually Looks Like

Moving away from a utility library does not mean going back to 2010-era stylesheets. Modern CSS is genuinely good. Here is a practical structure that scales:

1. Custom Properties at the Root

Define your design tokens once, in one place.

:root {
  --color-brand:    #1a56db;
  --color-surface:  #f9fafb;
  --space-md:       1rem;
  --space-lg:       1.5rem;
  --radius-card:    0.75rem;
  --font-body:      'Inter', system-ui, sans-serif;
}

Every component then references these variables. Change a token at the root and every consumer updates automatically — no find-and-replace across config files.

2. A Shallow, Purposeful Layer Order

Use CSS @layer (now baseline-supported across all modern browsers) to make specificity predictable:

  • base — resets and element defaults
  • tokens — custom property definitions
  • components — reusable UI patterns (.card, .btn, .form-field)
  • utilities — the handful of truly universal helpers you actually need (sr-only, visually-hidden, truncate)
  • overrides — per-page or per-context exceptions

This is not a new idea — it borrows from ITCSS and BEM — but @layer makes it enforceable without naming conventions alone.

3. Component Files That Own Their Own Styles

If you are using a component-based framework (React, Vue, Svelte, Angular), co-location still makes sense — just co-locate a scoped CSS file rather than inline utility classes. A Button.module.css file with ten meaningful rules is easier to audit than a className prop with thirty utilities.


When Tailwind Is Still the Right Call

Being honest about this matters. Tailwind remains an excellent choice when:

  • You are building a marketing site or landing page with a tight deadline and a single developer
  • Your team has deep Tailwind expertise and a well-maintained design system built on it
  • You are using a component library (like shadcn/ui or Headless UI) that is already designed around Tailwind — removing it creates more work than it saves
  • Prototyping speed is the primary metric and long-term maintainability is a secondary concern

The mistake is not using Tailwind. The mistake is using it by default, without evaluating what the project actually needs.


Choosing a CSS Architecture: A Simple Decision Framework

Ask these four questions before committing to a styling strategy:

  1. Who maintains this long-term? A solo developer and a five-person team have different needs.
  2. How design-system-driven is the UI? The more custom the design, the more a token-based CSS approach pays off.
  3. What is the expected lifespan? A two-week campaign site and a multi-year SaaS product deserve different investments.
  4. How much of the codebase is already in place? Migration cost is real. Incremental improvement often beats a full rewrite.

Source

This article was inspired by a trending discussion on Hacker News originating from: Moving away from Tailwind, and learning to structure my CSS by Julia Evans.


Why This Matters for Your Project

If you are building or scaling a SaaS product, your frontend architecture is a long-term investment, not a config file you set once and forget. The right CSS strategy reduces onboarding time for new developers, keeps design changes cheap, and prevents the slow accumulation of visual debt that makes redesigns so expensive. Whether you stay with Tailwind, migrate to structured vanilla CSS, or adopt CSS Modules, the discipline is the same: make your styling decisions explicit, centralised, and easy to change.