Back
Frontend

Why I Stopped Fighting CSS and Embraced Tailwind

January 15, 20269 min read
CSSTailwindFrontend

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:

css
/* 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 Card to ContentCard but the old card.css file lingers because nobody is sure if something else imports it.
  • Naming becomes political. Is it .card__footer-link or .card__footer .link? Every team invents its own micro-dialect of BEM.
  • Specificity creeps in. Someone nests a .card inside a .sidebar and suddenly needs .sidebar .card__title to override a font size. Three months later, !important appears.
  • 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:

tsx
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:

css
.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:

javascript
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:

css
.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:

tsx
<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:

tsx
<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:

css
@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:

tsx
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>
  );
}
css
.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.