Tailwind CSS: From Zero to Hero
You open a stylesheet you wrote six months ago and find .card, .card-v2, .card--compact and .dashboard-card-override. Nobody remembers which ones are still used, so nobody dares delete any of them. Every new feature adds another few hundred lines, and the file only ever grows.
Tailwind CSS attacks that problem by moving most styling into small utility classes in your markup. This post walks through installing it, how the utilities and spacing scale work, responsive design, states, customizing the config, a few component patterns, and the gotchas that bite people when they ship it.
Instead of inventing a class name and writing CSS for it, you put classes like p-4 and text-xl straight on the element. Your build only emits CSS for classes it actually finds in your templates, so the stylesheet stops growing with every feature. The cost is busier HTML.
How Tailwind differs from Bootstrap
Bootstrap and Foundation hand you finished components: a .btn, a .navbar, a .modal. Tailwind doesn't. It gives you low-level utility classes, one CSS property (or a small group) per class, and you compose the design yourself without leaving your HTML.
A rough analogy: Bootstrap is flat-pack furniture. It goes together fast and looks fine, but it looks like everyone else's. Tailwind is a workshop full of parts. It takes a bit longer to learn, but you can build the exact design your designer handed you instead of overriding someone else's defaults.
Named classes in a separate file vs. utilities on the element
What you actually get
Less context switching
You style the element where you write it, instead of jumping between a template and a stylesheet and inventing class names.
Small production CSS
Tailwind scans your templates and only generates the classes you use, so production CSS usually stays in the low tens of KB.
Configurable tokens
Colors, spacing, fonts and breakpoints live in tailwind.config.js, so you can match an existing design system.
Mobile-first prefixes
Breakpoint prefixes like md: keep responsive rules next to the base styles they modify.
Getting Started
There are a few ways to install Tailwind. The npm + PostCSS route below is the one most build setups (Vite, Laravel Mix, webpack) expect.
The steps below are the Tailwind v3 setup. Tailwind v4 ships a CSS-first config and a separate CLI package, and npx tailwindcss init no longer exists there, so on a fresh install you'll get an error like "could not determine executable to run". If you want to follow along exactly, install tailwindcss@3; for v4, follow the official install guide for your framework.
Install Tailwind via npm
Initialize your project and install Tailwind CSS along with its peer dependencies.
# Create a new project (if needed)
npm init -y
# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
# Generate config files
npx tailwindcss init -p
Configure Template Paths
Tell Tailwind where your HTML/JSX files are. It only generates classes it finds in these files.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,js,jsx,ts,tsx}",
"./public/index.html",
],
theme: {
extend: {},
},
plugins: [],
}
Add Tailwind Directives
Include Tailwind's layers in your main CSS file.
@tailwind base;
@tailwind components;
@tailwind utilities;
For a quick prototype you can use the Play CDN. Don't ship it: it compiles styles in the browser on every page load and logs a warning telling you so.
<script src="https://cdn.tailwindcss.com"></script>
Core Concepts
Utility Classes Explained
Every Tailwind class does one thing. Once you know the naming pattern you can usually guess a class name before you look it up:
Common utility class patterns and their CSS equivalents
The Spacing Scale
Spacing follows a scale in steps of 0.25rem (4px at the default font size). Because everyone picks from the same small set of values, gaps and paddings end up consistent without a designer policing them:
| Class | Pixels | REM | Use Case |
|---|---|---|---|
p-1 | 4px | 0.25rem | Tight spacing, icons |
p-2 | 8px | 0.5rem | Small buttons, badges |
p-4 | 16px | 1rem | Standard padding |
p-6 | 24px | 1.5rem | Card content |
p-8 | 32px | 2rem | Section spacing |
p-12 | 48px | 3rem | Large sections |
Responsive Design
Breakpoints are mobile-first. Unprefixed classes apply everywhere, and a prefix like md: means "from 768px up". The classic mistake is writing sm: and expecting it to target phones; it actually kicks in at 640px and above, so phone styles go on the unprefixed class.
Tailwind's mobile-first breakpoint system
<!-- Stack on mobile, side-by-side on larger screens -->
<div class="flex flex-col md:flex-row gap-4">
<!-- Full width on mobile, half on desktop -->
<div class="w-full md:w-1/2 p-4 md:p-6 lg:p-8">
<h2 class="text-xl md:text-2xl lg:text-3xl font-bold">
Responsive Heading
</h2>
<p class="text-sm md:text-base text-gray-600">
This text scales with the viewport.
</p>
</div>
<div class="w-full md:w-1/2">
<!-- Content -->
</div>
</div>
flex-col, w-full, text-xl
flex-row, w-1/2, text-2xl
p-8, text-3xl
Colors & Typography
The default palette has 22 colors, each with shades from 50 (lightest) to 950 (darkest). A sample of two of them:
<!-- Text colors -->
<p class="text-gray-600">Muted text</p>
<p class="text-blue-500">Primary accent</p>
<p class="text-red-600">Error message</p>
<!-- Background colors -->
<div class="bg-white dark:bg-gray-900">Theme-aware</div>
<button class="bg-blue-500 hover:bg-blue-600">Interactive</button>
<!-- Border colors -->
<input class="border border-gray-300 focus:border-blue-500" />
<!-- Gradient -->
<div class="bg-gradient-to-r from-purple-500 to-pink-500">
Gradient background
</div>
Typography Utilities
| Category | Classes | Example |
|---|---|---|
| Font Size | text-xs to text-9xl |
text-lg = 1.125rem |
| Font Weight | font-thin to font-black |
font-semibold = 600 |
| Line Height | leading-none to leading-loose |
leading-relaxed = 1.625 |
| Letter Spacing | tracking-tighter to tracking-widest |
tracking-wide = 0.025em |
| Text Align | text-left, text-center, text-right |
text-justify |
Flexbox & Grid
Layout is where I find Tailwind saves the most time, because flex and grid setups are short to write inline. These are the patterns I reach for most:
Common Flexbox Patterns
<!-- Center everything -->
<div class="flex items-center justify-center h-screen">
Perfectly centered!
</div>
<!-- Space between items -->
<nav class="flex items-center justify-between p-4">
<logo />
<menu />
</nav>
<!-- Responsive card row -->
<div class="flex flex-wrap gap-4">
<div class="flex-1 min-w-[300px]">Card 1</div>
<div class="flex-1 min-w-[300px]">Card 2</div>
<div class="flex-1 min-w-[300px]">Card 3</div>
</div>
CSS Grid Layouts
<!-- Responsive grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
<!-- Auto-fit responsive grid -->
<div class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
<!-- Cards auto-arrange -->
</div>
<!-- Dashboard layout -->
<div class="grid grid-cols-12 gap-4">
<aside class="col-span-3">Sidebar</aside>
<main class="col-span-9">Content</main>
</div>
States & Interactivity
Hover, focus, active and dark mode all use the same prefix idea as breakpoints:
<!-- Hover state -->
<button class="bg-blue-500 hover:bg-blue-600 transition-colors">
Hover me
</button>
<!-- Focus state -->
<input class="border-gray-300 focus:border-blue-500 focus:ring-2
focus:ring-blue-200 outline-none transition" />
<!-- Active state -->
<button class="bg-blue-500 active:bg-blue-700 active:scale-95
transition-transform">
Click me
</button>
<!-- Group hover (parent-child relationship) -->
<div class="group p-4 hover:bg-gray-100">
<h3 class="group-hover:text-blue-500">Card Title</h3>
<span class="opacity-0 group-hover:opacity-100 transition">
→
</span>
</div>
<!-- Dark mode -->
<div class="bg-white dark:bg-gray-900 text-black dark:text-white">
Theme-aware content
</div>
Modifiers stack: dark:hover:bg-gray-800 applies only in dark mode while hovering. Also note that dark: follows the OS setting by default. If your site has a manual theme toggle, set darkMode: 'class' (or 'selector' in v3.4+) in the config, or the toggle will appear to do nothing.
Customization
Most real projects need their own brand colors and fonts. Put them under theme.extend in tailwind.config.js so you add to the defaults; anything placed directly under theme replaces that whole section instead:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{html,js,jsx}"],
theme: {
extend: {
// Custom colors
colors: {
brand: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
primary: '#0ea5e9',
},
// Custom fonts
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
display: ['Cabinet Grotesk', 'sans-serif'],
},
// Custom spacing
spacing: {
'128': '32rem',
'144': '36rem',
},
// Custom breakpoints
screens: {
'xs': '475px',
'3xl': '1920px',
},
// Custom animations
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}
Component Patterns
Tailwind ships no components, so you'll end up writing your own. Here are three common ones to copy and adapt:
Card Component
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg
overflow-hidden hover:shadow-xl transition-shadow">
<img src="cover.jpg" class="w-full h-48 object-cover" />
<div class="p-6">
<span class="text-sm text-blue-500 font-medium">Category</span>
<h3 class="text-xl font-bold mt-2 mb-3">Card Title</h3>
<p class="text-gray-600 dark:text-gray-300">
Description text goes here...
</p>
<button class="mt-4 px-4 py-2 bg-blue-500 text-white
rounded-lg hover:bg-blue-600 transition">
Learn More
</button>
</div>
</div>
Responsive Navbar
<nav class="bg-white dark:bg-gray-900 shadow-sm fixed w-full
top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<a href="/" class="text-xl font-bold">Logo</a>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center gap-8">
<a href="#" class="text-gray-600 hover:text-black
transition">Home</a>
<a href="#" class="text-gray-600 hover:text-black
transition">About</a>
<a href="#" class="text-gray-600 hover:text-black
transition">Contact</a>
<button class="bg-blue-500 text-white px-4 py-2
rounded-lg hover:bg-blue-600">
Get Started
</button>
</div>
<!-- Mobile Menu Button -->
<button class="md:hidden p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor">
<path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
</div>
</nav>
Form Input
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700
dark:text-gray-300">
Email Address
</label>
<input type="email"
class="w-full px-4 py-3 rounded-lg border border-gray-300
focus:border-blue-500 focus:ring-2 focus:ring-blue-200
dark:bg-gray-800 dark:border-gray-600
dark:focus:ring-blue-900 outline-none transition"
placeholder="you@example.com" />
<p class="text-sm text-gray-500">We'll never share your email.</p>
</div>
Best Practices
Do This
- Use
@applysparingly, only for patterns repeated in many places - Create component files for reusable UI pieces
- Install the Tailwind IntelliSense extension in VS Code
- Use arbitrary values
[...]for one-off values - Enable dark mode from the start
Avoid This
- Don't use
@applyfor everything; you end up rebuilding the stylesheet you were trying to avoid - Don't fight the framework with custom CSS
- Don't build class names with string concatenation like
`bg-${color}-500` - Don't use arbitrary values when a scale value exists
- Don't leave files out of the
contentpaths
This is the most common Tailwind bug report I see. Tailwind doesn't run your JavaScript; it scans files for complete class strings. So `text-${status}-600` never shows up as text-red-600, and no CSS is generated for it. It often seems to work locally only because the same class happens to be used somewhere else. Map values to full class names instead ({ error: 'text-red-600', ok: 'text-green-600' }), or add them to safelist in the config.
Plugins & Tools Worth Installing
@tailwindcss/forms
Provides better default styling for form elements like inputs, selects, and checkboxes.
@tailwindcss/typography
The prose class, which gives sensible typography to HTML you don't control, like CMS content or rendered markdown.
@tailwindcss/aspect-ratio
Tailwind v3 has built-in aspect-video and aspect-square using the native CSS property, so you only need this plugin for very old browsers.
Tailwind CSS IntelliSense
VSCode extension for autocomplete, syntax highlighting, and linting.
- Ensure
contentpaths in config include all template files - Enable CSS minification in your build process
- Check the output size; a typical site lands well under 50KB gzipped, so a much bigger file usually means something is misconfigured
- Test on actual devices, not just Chrome DevTools
When I'd use Tailwind (and when I wouldn't)
I'd pick it for apps built from components, like React, Vue, Blade or Livewire. The long class lists live in one component file, so you write them once and reuse the component. It also fits teams where several people touch the UI, because the spacing and color scales stop everyone inventing their own values.
Where I'd think twice:
- Content-heavy sites with markup you don't control. If most of the HTML comes from a CMS or markdown, you can't put utilities on it. The typography plugin helps, but you're mostly writing regular CSS at that point.
- Projects without a build step. Tailwind needs to scan files and generate CSS. If you can't add that to the pipeline, the Play CDN isn't a production answer.
- Teams that hate long class attributes. The markup really is noisier, and diffs on class strings can be hard to review. If the team won't extract components, you get the same card copy-pasted in twelve places with slight differences.
- An existing large stylesheet. Adding Tailwind on top means two systems, and the base reset (Preflight) can restyle headings and lists you didn't expect. Migrate gradually, or switch off
preflightincorePluginswhile both exist.
If you're unsure, try it on one small project first. The verbose HTML feels wrong for the first few days; whether it still bothers you after a week is a good sign of whether it suits you. The official docs are good and searchable, so keep them open while you learn the class names.