March 26, 20265 min read

Tailwind CSS: Why Utility-First Works (Even Though It Looks Wrong)

A practical guide to Tailwind CSS covering setup, essential classes, responsive design, dark mode, @apply extraction, and why the 'ugly HTML' argument misses the point.

css tailwind frontend web-development
Ad 336x280

The first time you see Tailwind CSS, you'll probably hate it. A

with fifteen classes crammed into it looks objectively worse than a clean
. Every instinct says this is wrong. Separation of concerns. Semantic class names. Everything you learned about CSS best practices.

Then you build something real with it, and you don't want to go back.

Why Utility-First Actually Works

The traditional CSS approach sounds great in theory: write semantic class names, keep styles in separate files, reuse classes across components. In practice, here's what happens:

  1. You write .card-header for the first card component
  2. A second card needs slightly different styles, so you write .card-header-alt or .card-header--large
  3. Six months later, nobody knows which classes are still used
  4. Your CSS file is 4,000 lines and everyone's afraid to delete anything
Tailwind flips this around. Instead of naming your styles, you compose them directly. When you delete a component, its styles go with it. No dead CSS. No naming debates. No specificity wars.

The "ugly HTML" complaint is real but misplaced. You're not reading raw HTML files — you're reading components. A React component called is just as semantic as a

. The styling is co-located with the markup, which is where you want it.

Getting Started

Install Tailwind in a new project:

npm install -D tailwindcss @tailwindcss/postcss postcss

Add it to your postcss.config.js:

module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

Import Tailwind in your main CSS file:

@import "tailwindcss";

That's it. No configuration file needed for basic usage — Tailwind v4 auto-detects your content files.

Essential Classes You'll Use Constantly

Layout:
  • flex, grid, block, hidden
  • items-center, justify-between, gap-4
  • w-full, max-w-xl, h-screen
Spacing:
  • p-4 (padding 1rem all sides), px-6 (horizontal padding), py-2 (vertical)
  • m-auto, mt-8, mb-4
  • The scale: 1 = 0.25rem, 2 = 0.5rem, 4 = 1rem, 8 = 2rem
Typography:
  • text-sm, text-lg, text-2xl
  • font-bold, font-medium
  • text-gray-700, text-blue-500
  • leading-relaxed, tracking-wide
Borders and shadows:
  • rounded-lg, rounded-full
  • border, border-gray-200
  • shadow-md, shadow-lg
Backgrounds:
  • bg-white, bg-gray-50, bg-blue-600
  • bg-gradient-to-r from-blue-500 to-purple-600
You learn these by using them. After a week, the most common ones are muscle memory.

Responsive Design

Tailwind uses mobile-first breakpoints as prefixes:

<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <!-- 1 column on mobile, 2 on tablet, 3 on desktop -->
</div>

Breakpoints: sm (640px), md (768px), lg (1024px), xl (1280px), 2xl (1536px).

No media queries to write. No separate mobile stylesheet. The responsive behavior lives right next to the base styles, which makes it trivial to understand what a component looks like at each breakpoint.

Dark Mode

Add dark: prefix to any class:

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
  <h2 class="text-blue-600 dark:text-blue-400">Title</h2>
</div>

By default, Tailwind uses the prefers-color-scheme media query. If you want manual toggle control, set the dark mode strategy to selector in your config and toggle a dark class on the element.

Hover, Focus, and Other States

State variants work like responsive prefixes:

<button class="bg-blue-600 hover:bg-blue-700 focus:ring-2 focus:ring-blue-500
               active:bg-blue-800 disabled:opacity-50 disabled:cursor-not-allowed">
  Submit
</button>

Group hover is useful for card-style interactions:

<div class="group cursor-pointer">
  <h3 class="group-hover:text-blue-600 transition-colors">Card Title</h3>
  <p class="group-hover:text-gray-700">Description</p>
</div>

@apply: Use Sparingly

Tailwind provides @apply to extract utility patterns into CSS classes:

.btn-primary {
  @apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700
         focus:ring-2 focus:ring-blue-500 transition-colors;
}

This is tempting. Resist using it for everything. If you extract every repeated pattern into an @apply class, you're just writing CSS with extra steps and losing the benefits of utility-first.

Good uses of @apply:
  • Base typography styles applied to markdown/prose content
  • Third-party component overrides where you can't add classes to the HTML
Bad uses of @apply:
  • Creating .card, .header, .sidebar classes — use components instead
  • Anything you could solve with a React/Vue/Svelte component
If you're using a component framework (and you probably are), the component itself is your abstraction. You don't need .card when you have .

Tailwind vs Traditional CSS: The Real Trade-off

Tailwind is faster for building UIs. Significantly faster. You stay in one file, you don't context-switch between HTML and CSS, and the constraint system (fixed spacing scale, predefined colors) makes design consistency almost automatic.

The trade-off is readability of individual elements. A

Ad 728x90