What is Tailwind CSS? The Utility-First CSS Framework Developers Either Love or Hate
Tailwind is a utility-first CSS framework. You style elements by stacking single-purpose class names directly in your markup. Half the dev community hated this idea, then started using it. Here's why it works and what you need to know.
Tailwind CSS is a utility-first CSS framework. You style elements by applying small, single-purpose class names directly in your markup. No writing CSS files. No inventing class names. No hunting through stylesheets to find where a style came from.
When you first see it, the markup looks wrong:
<button class="bg-blue-600 text-white font-semibold px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
Save Changes
</button>
That's a lot of classes on one element. Developers who've been writing CSS the traditional way have an immediate reaction: this violates separation of concerns. This is HTML and CSS crammed together in a way that looks like early 2000s inline styles.
Then they use it for two weeks on a real project and stop going back.
The Problem With Traditional CSS at Scale
Writing CSS component-by-component works fine when a project is small. As applications grow, the codebase breaks down in predictable ways.
You write .button-primary for one context. Someone else writes .btn-main for another. Three months later the codebase has .card-button, .action-btn-blue, and .submit-button-large. The specificity wars start. Someone adds !important. The global stylesheet becomes a file nobody wants to touch because touching it breaks something they didn't expect.
CSS-in-JS solutions like styled-components and Emotion solved the scoping problem. They added runtime overhead, complicated server-side rendering, and split your styles from the markup you're reading. CSS Modules solved scoping without the runtime cost but kept the context-switching between files.
Tailwind's answer: stop naming things. Apply styles directly in the markup. If you delete a component, its styles disappear with it. No dead CSS accumulates.
How Utility-First Actually Works
Tailwind ships a large set of utility classes that each map to a single CSS declaration.
| Class | CSS |
|---|---|
flex |
display: flex |
p-4 |
padding: 1rem |
text-lg |
font-size: 1.125rem |
bg-gray-100 |
background-color: rgb(243 244 246) |
rounded-md |
border-radius: 0.375rem |
hover:bg-gray-200 |
background-color: rgb(229 231 235) on hover |
Stack them to style elements:
<div class="flex items-center gap-4 p-6 bg-white rounded-xl shadow-sm border border-gray-200">
<img class="w-12 h-12 rounded-full object-cover" src="avatar.jpg" />
<div>
<p class="text-sm font-medium text-gray-900">Jane Smith</p>
<p class="text-sm text-gray-500">Product Engineer</p>
</div>
</div>
That's a user card. Every style lives in the HTML. You read it and you know exactly what it looks like without opening a CSS file.
Installation With Vite
Tailwind v4 ships a dedicated Vite plugin that simplifies setup considerably:
npm install -D tailwindcss @tailwindcss/vite
In vite.config.ts:
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
tailwindcss(),
react(),
],
});
In your main CSS file:
@import "tailwindcss";
That's the entire setup for Vite. Previous versions of Tailwind required a tailwind.config.js file and separate PostCSS configuration. Tailwind v4 eliminated most of that.
The Design System Built In
Tailwind isn't random utility classes. It's built on a consistent design system.
The spacing scale uses multiples of 0.25rem: p-1 is 0.25rem, p-2 is 0.5rem, p-4 is 1rem, p-8 is 2rem. Your spacing is consistent by default without you thinking about it.
The color palette covers every major color from shade 50 (near white) through 950 (near black). blue-600 is the same blue you'll see in bg-blue-600, text-blue-600, and border-blue-600. Colors stay coherent across your UI.
The type scale covers text-xs through text-9xl with matching line heights.
Use the defaults and your UI looks considered. Override them when you need brand-specific values.
Responsive Design
Responsive classes use prefix modifiers:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
One column on mobile. Two columns at 768px. Three columns at 1024px. No media queries written by hand, no separate stylesheet for breakpoints.
The breakpoints: sm (640px), md (768px), lg (1024px), xl (1280px), 2xl (1536px). All configurable in your CSS file if your design system uses different values.
Dark Mode
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
The dark: prefix applies styles when dark mode is active. Configure it in your CSS:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
Add the dark class to your <html> element to toggle it. Or use prefers-color-scheme for automatic OS-level dark mode detection. Tailwind handles both.
Component Extraction in React
Duplicating 15 classes on every button across your codebase is not sustainable. In React, the abstraction lives in the component, not in a CSS class.
type ButtonVariant = 'primary' | 'secondary' | 'destructive';
const variantClasses: Record<ButtonVariant, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
destructive: 'bg-red-600 text-white hover:bg-red-700',
};
function Button({ variant = 'primary', children, ...props }) {
return (
<button
className={`px-4 py-2 rounded-lg font-medium transition-colors ${variantClasses[variant]}`}
{...props}
>
{children}
</button>
);
}
The Button component is the reusable abstraction. You don't need a .btn-primary CSS class. The variant logic lives in JavaScript where it belongs, and the styles travel with the component.
If you're writing plain HTML instead of React, Tailwind's @apply directive extracts repeated patterns into CSS classes:
@layer components {
.btn-primary {
@apply bg-blue-600 text-white font-medium px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors;
}
}
Use @apply sparingly. When you find yourself creating many custom component classes, you're rebuilding the CSS-naming problem Tailwind was designed to eliminate.
The "Separation of Concerns" Argument
The objection: Tailwind mixes styling with markup. That violates separation of concerns.
The counter: in a component-based application, the component is the separation. A Button.jsx file shouldn't be decoupled from its styles. They describe the same thing. Splitting them across files added indirection, not clarity.
The traditional HTML + CSS separation made sense when pages were static content with styling layered on top. Components changed the model. A component has markup, styles, and behavior. They belong together.
CSS-in-JS made this same argument years ago. Tailwind delivers the same benefit without a runtime, without complications to server-side rendering, and with better IDE tooling support.
What Tailwind CSS Is Not
Tailwind doesn't give you pre-built UI components. No <Modal>. No <DataTable>. You build those yourself using the utility classes.
For a component library that works with Tailwind:
Shadcn/UI is the current go-to. It's not a library you install; it's a collection of accessible, copy-paste components built with Tailwind and Radix UI primitives. You own the code, which means you modify it directly rather than fighting with a theme system.
Headless UI provides accessible, unstyled component primitives from the Tailwind team itself. You write the styles.
Radix UI is similar to Headless UI. Solid accessibility guarantees, zero styling opinions.
Most new React design systems in 2026 use some combination of Tailwind and Radix or Tailwind and Shadcn.
The IntelliSense Factor
One legitimate complaint about Tailwind is memorizing hundreds of class names. The Tailwind CSS IntelliSense extension for VS Code solves this completely. Install it and you get autocomplete for every utility class, color previews, and hover documentation. You stop memorizing class names within a week and start typing them on muscle memory.
It also shows you the generated CSS for any class on hover. You always know what a class produces without leaving your editor.
The Conclusion
Tailwind CSS is the dominant styling approach for new React projects in 2026. Teams kept choosing it, the codebases stayed manageable, and the pattern spread. I've shipped large applications with plain CSS, CSS Modules, styled-components, and Tailwind. The Tailwind codebases were easier to change six months after writing them. The styles stayed local. Dead code didn't pile up.
If you're starting something new, add Tailwind from day one. The learning curve is a few hours of memorizing class names. The IntelliSense extension cuts even that short.