← Back to blog

Tailwind CSS: why we use it in all our projects

2026-06-03

Tailwind CSS: Why We Use It in All Our Projects

If you have spent any time in frontend development over the past five years, you have probably encountered Tailwind CSS. Perhaps you dismissed it as "just another CSS framework." Perhaps the utility-first approach felt verbose and unclean. We felt the same way initially. Yet today, Tailwind CSS is the foundational styling layer in every project we ship. This post explains why we made that decision, what trade-offs we accepted, and how we use it in practice with React and modern frontend architectures.

The Problem with Traditional CSS at Scale

Before Tailwind, our teams wrote traditional CSS. We followed BEM methodology, maintained separate stylesheets, and debated naming conventions in pull requests. The problems accumulated predictably.

CSS files grew without bound. A component might import styles from three different files. Global namespace collisions required increasingly elaborate class names. Developers hesitated to delete styles because something somewhere might depend on them. Our CSS bundle sizes reflected this fear: code that was technically "dead" remained in production.

Consider a typical BEM approach for a card component:

/* card.css */
.card { display: flex; flex-direction: column; }
.card--featured { border-color: gold; }
.card__title { font-size: 1.25rem; font-weight: 700; }
.card__description { color: #6b7280; margin-top: 0.5rem; }

The corresponding React component imports this stylesheet. But where does the stylesheet live? Is it co-located? In a shared directory? When a designer requests a minor adjustment like changing margin-top from 0.5rem to 0.75rem, a developer must edit two files, run the build, and verify no other components broke.

We wanted styling decisions to live colocated with markup, to be explicit rather than implicit, and to be governed by a design system rather than arbitrary values.

Utility-First: The Mental Model Shift

Tailwind CSS inverts traditional CSS architecture. Instead of writing classes that describe what something is (a card, a button, a modal), you compose utilities that describe what it looks like. The same card becomes:

function Card({ featured, title, description }) {
  return (
    <div className={\`
      flex flex-col
      \${featured ? 'border-yellow-400' : 'border-gray-200'}
    \`}>
      <h2 className="text-xl font-bold">{title}</h2>
      <p className="text-gray-500 mt-2">{description}</p>
    </div>
  );
}

No separate stylesheet. No inventing class names. No hunting through files to understand why a style applies. The styles are right there in the markup.

Critics call this "mixing concerns." We disagree. The separation of concerns principle was never about file organization; it was about independent evolution. In practice, our components' markup and styling change together. Co-location reduces cognitive load.

Design System Enforcement Through Constraints

Tailwind's most underrated feature is its constraint system. The default configuration exposes a curated set of values: specific spacing scales, color palettes, font sizes, shadows. You cannot arbitrarily write margin: 13px without explicit configuration.

This transforms how teams collaborate with designers. When our design system specifies 8px base spacing, Tailwind's -1 through -96 scale enforces it. A developer reaching for mt-[13px] signals either a legitimate exception worth discussing or a misunderstanding to correct.

We customize our tailwind.config.js to match our design tokens precisely:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          500: '#3b82f6',
          900: '#1e3a8a',
        }
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      }
    }
  }
}

Now text-brand-500 and font-sans are available throughout the application, consistent and type-safe if integrated with tools like TypeScript.

Developer Velocity in Practice

We measure velocity not in lines written but in experiments shipped. Tailwind accelerates this dramatically. When prototyping a new feature, we modify markup and see visual results instantly. No context switching between files. No inventing semantic names for one-off layouts.

Consider building a responsive navigation bar. With traditional CSS, this involves media queries, flexbox declarations, and careful z-index management. With Tailwind:

function Navbar() {
  return (
    <nav className="bg-white border-b border-gray-200">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between h-16">
          <div className="flex items-center">
            <Logo className="h-8 w-auto" />
          </div>
          <div className="hidden md:flex items-center space-x-8">
            <a href="/products" className="text-gray-600 hover:text-brand-500 transition-colors">
              Products
            </a>
            <a href="/pricing" className="text-gray-600 hover:text-brand-500 transition-colors">
              Pricing
            </a>
          </div>
          <div className="flex items-center md:hidden">
            <MobileMenu />
          </div>
        </div>
      </div>
    </nav>
  );
}

The responsive behavior is explicit: hidden md:flex means this container is hidden by default, flex at the medium breakpoint. The breakpoint values come from our centralized configuration, not scattered media queries. Hover states use standard hover: prefixes rather than separate CSS rules.

Managing Complexity: Extracting Components and Patterns

Veterans of utility-first frameworks eventually encounter "className soup"—lengthy strings of utilities that become unwieldy. We mitigate this through deliberate abstraction layers.

First, Tailwind's @apply directive extracts repeated patterns when necessary, though we use it sparingly:

/* Only for genuinely global, repeated patterns */
@layer components {
  .btn-primary {
    @apply inline-flex items-center justify-center px-4 py-2 
           border border-transparent text-sm font-medium rounded-md 
           text-white bg-brand-500 hover:bg-brand-600 
           focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500;
  }
}

More commonly, we compose React components that encapsulate styling:

function Button({ variant = 'primary', size = 'md', children, ...props }) {
  const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2';
  
  const variants = {
    primary: 'text-white bg-brand-500 hover:bg-brand-600 focus:ring-brand-500',
    secondary: 'text-brand-700 bg-brand-100 hover:bg-brand-200 focus:ring-brand-500',
    ghost: 'text-gray-600 hover:bg-gray-100 focus:ring-gray-500',
  };
  
  const sizes = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-sm',
    lg: 'px-6 py-3 text-base',
  };

  return (
    <button 
      className={\`\${baseStyles} \${variants[variant]} \${sizes[size]}\`}
      {...props}
    >
      {children}
    </button>
  );
}

This pattern—varying styles through data rather than arbitrary class strings—keeps our components maintainable and testable.

The Build Step: PurgeCSS and Production Optimization

A common concern about utility frameworks is bundle size. With thousands of possible utilities, won't the CSS be enormous?

Tailwind solves this through Just-In-Time (JIT) compilation. The build process scans your source files, identifies only the utilities you actually use, and generates a stylesheet containing precisely those rules. Our production CSS bundles typically range from 10-20KB gzipped, smaller than most手写 CSS we previously shipped.

The configuration is straightforward:

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './public/index.html',
  ],
  // JIT mode is default in Tailwind v3+
}

This content array tells Tailwind where to look for class names. We include component directories, template files, and any dynamic values. The build fails fast if we miss a source, which we prefer to silent bloat.

Dark Mode Without the Headaches

Supporting dark mode previously required extensive CSS custom properties or duplicated theme files. Tailwind's dark: variant makes this trivial:

function FeatureCard({ title, description }) {
  return (
    <div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
      <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
        {title}
      </h3>
      <p className="mt-2 text-gray-600 dark:text-gray-400">
        {description}
      </p>
    </div>
  );
}

We configure dark mode detection strategy in our Tailwind config:

// tailwind.config.js
module.exports = {
  darkMode: 'class', // or 'media' for OS-level preference
}

The class strategy lets us toggle dark mode programmatically—useful for user preference persistence—while media respects system settings automatically.

Integration with Modern Tooling

Tailwind integrates seamlessly with our build pipeline. For React applications using Vite:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  css: {
    postcss: {
      plugins: [require('tailwindcss'), require('autoprefixer')],
    },
  },
});

For Next.js, the built-in PostCSS configuration picks up tailwind.config.js automatically. We have standardized this setup across our project templates, reducing onboarding friction for new team members.

When We Do Not Use Tailwind

An honest assessment requires acknowledging limits. We avoid Tailwind for:

These are exceptions. The rule remains: default to Tailwind, justify deviations.

The Bottom Line

Tailwind CSS is not merely a styling tool. It is a design system implementation strategy, a collaboration protocol between designers and developers, and a performance optimization for production bundles.

Our teams ship faster with fewer bugs. Code reviews focus on behavior rather than "did you remember to update the stylesheet." New contributors understand our visual language through the config file rather than tribal knowledge.

If you have resisted utility-first CSS, we understand. We resisted too. The learning curve is real: you must internalize the naming conventions, configure your design tokens thoughtfully, and develop discipline around component extraction. The investment pays dividends in maintainability and velocity.

Start with a small project. Configure your colors and spacing scale. Build one component without touching a .css file. The productivity shift is subtle at first, then unmistakable. That is why Tailwind CSS lives in every project we ship.