tailwindcss
Tailwind CSS is a utility-first CSS framework: instead of writing custom stylesheets you compose small single-purpose classes like flex, pt-4, and text-center directly in your markup. A build step scans your source files for class names and generates only the CSS you actually use, so the shipped stylesheet stays small even though the framework covers most of modern CSS. Since v4, configuration lives in CSS itself (the @theme block) rather than a JavaScript config file.
Still the default answer for styling new web projects, with an ecosystem no competitor matches. Pick it for new work; think twice before migrating a large, stable v3 codebase until you have a concrete reason.
Use it if
- You want spacing, color, and type scales enforced by a design-token system instead of ad hoc CSS values scattered across files
- Your team ships React, Vue, or server-rendered templates and is tired of naming, organizing, and dead-code-hunting CSS files
- You rely on copy-paste component ecosystems like shadcn/ui or Tailwind UI, which assume Tailwind classes
- You are on v4-era tooling (Vite, PostCSS, or the standalone CLI) and can adopt the CSS-first @theme configuration
- Your markup is generated by a CMS or a third party you do not control; Tailwind can only style class attributes you can edit
- Long class strings in markup bother your team more than separate stylesheets do; that tradeoff is permanent, not a learning-curve phase
- You have a large stable v3 codebase: v4 replaced tailwind.config.js with CSS config, and plenty of plugins, tutorials, and answers online still assume v3 syntax
- You are styling HTML email, where most clients need inlined CSS rather than a utility build step
Setup reality
Fresh v4 setup on Vite is genuinely short: install tailwindcss and @tailwindcss/vite, add the plugin, and put a single @import "tailwindcss" line in your CSS. The real cost is migration and stale knowledge: the PostCSS plugin moved to a separate @tailwindcss/postcss package, tailwind.config.js gave way to @theme blocks in CSS, and a large share of tutorials still show v3. The scanner also only sees class names that appear literally in source, so dynamically concatenated class strings silently produce no CSS, which surprises every team exactly once.
Patterns
Install with Vite (v4)install-vite
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})
/* src/app.css */
@import "tailwindcss";v4 has no init command and no tailwind.config.js by default; the CSS import is the whole entry point.
Install with PostCSSinstall-postcss
npm install tailwindcss @tailwindcss/postcss postcss
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}In v4 the PostCSS plugin lives in @tailwindcss/postcss, not in the tailwindcss package; old v3 configs break here first.
Define design tokens in CSStheme-tokens
@import "tailwindcss";
@theme {
--color-brand: oklch(0.65 0.2 250);
--font-display: "Inter", sans-serif;
--spacing-gutter: 2.5rem;
}
/* now available as bg-brand, font-display, p-gutter */@theme replaces the theme section of the old tailwind.config.js; tokens also become plain CSS variables you can read anywhere.
Class-toggled dark modedark-mode-class
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
<!-- markup -->
<html class="dark">
<body class="bg-white dark:bg-gray-950">By default dark: follows prefers-color-scheme; you need @custom-variant to switch it to a class toggle.
Add a custom utilitycustom-utility
@utility content-auto {
content-visibility: auto;
}
<!-- works with variants too -->
<div class="content-auto lg:content-auto">@utility registers the class with the engine, so variants like hover: and lg: work; a plain CSS class would not get them.
One-off arbitrary valuesarbitrary-values
<div class="top-[117px] grid-cols-[1fr_500px_2fr] bg-[#1da1f2]">
...
</div>Fine for one-offs, but if the same bracket value keeps repeating, promote it to a @theme token.
Scan extra source pathsscan-extra-sources
@import "tailwindcss";
/* pull class names from a package that ships Tailwind markup */
@source "../node_modules/@acme/ui";v4 auto-detects your project files but ignores node_modules unless you add a path with @source explicitly.
Conditional classes that survive the scannerdynamic-classes
// good: full class names appear literally in source
const tone = ok ? 'bg-green-600' : 'bg-red-600'
// bad: scanner never sees the full name, no CSS is generated
const tone2 = 'bg-' + color + '-600'The scanner matches literal strings in source files; any runtime string building produces class names with no CSS behind them.
Reuse styles with @applyapply-reuse
/* app.css */
.btn {
@apply rounded-lg px-4 py-2 font-semibold text-white;
}Using @apply from a separate CSS file may need @reference at the top so it can see your theme; overusing it also recreates the naming problem Tailwind exists to remove.
Upgrade a v3 projectupgrade-v3-to-v4
npx @tailwindcss/upgradeThe codemod handles most of the config-to-CSS migration; run it on a clean git branch and review the diff, plugin-heavy configs need manual work.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| unocss | npm | Same utility-first idea with an on-demand engine and fully custom rule presets |
| bootstrap | npm | Prebuilt components with no build-step scanning, when you want standard-looking pages up fast |
| open-props | npm | Plain CSS custom properties when you want design tokens without utility classes |