
Introduction
Tailwind is ugly in your HTML and beautiful in production. If the class soup bothers you, you're looking at the wrong thing — look at the CSS bundle size and the iteration speed instead.
You already know what p-4 does. You don't need another tutorial explaining utility classes. What you probably need — what most people actually struggle with — is the responsive system. How sm: and md: and lg: interact. When to reach for grid versus flex. Why your layout breaks at exactly 768px and nowhere else. The mental model for mobile-first design that makes responsive Tailwind feel intuitive instead of like guesswork.
That's what this is about. The examples target Tailwind CSS v4 (4.0 shipped in January 2025), with notes where v3 differs.
Utility-First Philosophy
The traditional CSS model has a fatal flaw that nobody talks about honestly: stylesheets only grow. They never shrink. Your team creates .card-container, .card-header, .card-body and defines their styles in a separate file. Six months later, nobody can delete a single rule because nobody knows what depends on it.
Tailwind kills the problem by eliminating custom class names entirely. Styles live in the markup. Nothing to orphan. Nothing to go stale.
Quick comparison.
<!-- HTML -->
<div class="alert-card">
<h3 class="alert-title">Warning</h3>
<p class="alert-message">Check your settings.</p>
</div>
<!-- CSS (separate file) -->
.alert-card {
padding: 1rem;
border-radius: 0.5rem;
background-color: #fef3c7;
border: 1px solid #f59e0b;
}
.alert-title {
font-weight: 700;
color: #92400e;
}
.alert-message {
color: #78350f;
}<div class="p-4 rounded-lg bg-amber-100 border border-amber-400">
<h3 class="font-bold text-amber-800">Warning</h3>
<p class="text-amber-900">Check your settings.</p>
</div>
<!-- No separate CSS file needed -->Self-contained. You read the markup and you know what it looks like. No hunting through stylesheets.
BEM, SMACSS — these exist to manage a problem that utility-first CSS just eliminates. The problem disappears rather than being managed. And honestly, that's a better outcome than any naming convention ever achieved.
Flexbox Layouts with Tailwind
Flex for one-dimensional stuff. Row of cards. Nav links. Button groups. Add flex to the container, control the rest with utilities.
<div class="flex flex-wrap justify-center gap-6 p-8">
<div class="flex flex-col items-center p-6 bg-white
rounded-xl shadow-md w-64">
<div class="w-12 h-12 bg-blue-100 rounded-lg
flex items-center justify-center mb-4">
<span class="text-blue-600 text-xl">⚡</span>
</div>
<h3 class="font-semibold text-gray-800 mb-2">Fast</h3>
<p class="text-gray-500 text-sm text-center">
Optimized builds for production performance.
</p>
</div>
<div class="flex flex-col items-center p-6 bg-white
rounded-xl shadow-md w-64">
<div class="w-12 h-12 bg-green-100 rounded-lg
flex items-center justify-center mb-4">
<span class="text-green-600 text-xl">⚙</span>
</div>
<h3 class="font-semibold text-gray-800 mb-2">Flexible</h3>
<p class="text-gray-500 text-sm text-center">
Customize everything to match your design.
</p>
</div>
<div class="flex flex-col items-center p-6 bg-white
rounded-xl shadow-md w-64">
<div class="w-12 h-12 bg-purple-100 rounded-lg
flex items-center justify-center mb-4">
<span class="text-purple-600 text-xl">🔧</span>
</div>
<h3 class="font-semibold text-gray-800 mb-2">Modular</h3>
<p class="text-gray-500 text-sm text-center">
Compose utilities to build any component.
</p>
</div>
</div>The two-level nesting pattern matters more than the individual classes. Outer container: row layout, wrapping, gap. Inner cards: column layout, centering. Once you see it, you can't unsee it — every card-based layout in every Tailwind project uses this exact structure. gap-6 over margin hacks. Always.
Grid Layouts and Auto-Placement
Grid is for two dimensions. Rows and columns at the same time. And the mental model switch from flex to grid trips people up because flex is about content flowing in a direction while grid is about defining slots and placing things into them. Different problem, different tool.
Here's a dashboard with cards that span different column counts — the thing you actually need grid for, because flex makes this painful.
<div class="grid grid-cols-4 gap-6 p-8">
<!-- Wide card spanning 2 columns -->
<div class="col-span-2 bg-linear-to-r from-blue-500
to-purple-600 rounded-2xl p-6 text-white">
<h3 class="text-xl font-bold mb-2">Revenue</h3>
<p class="text-3xl font-extrabold">$48,200</p>
<p class="text-blue-100 text-sm mt-1">+12.5% from last month</p>
</div>
<!-- Standard single-column cards -->
<div class="bg-white rounded-2xl p-6 shadow-xs
border border-gray-100">
<h3 class="text-gray-500 text-sm font-medium">Users</h3>
<p class="text-2xl font-bold text-gray-900 mt-1">2,340</p>
</div>
<div class="bg-white rounded-2xl p-6 shadow-xs
border border-gray-100">
<h3 class="text-gray-500 text-sm font-medium">Orders</h3>
<p class="text-2xl font-bold text-gray-900 mt-1">1,120</p>
</div>
<!-- Full-width card spanning all 4 columns -->
<div class="col-span-4 bg-white rounded-2xl p-6
shadow-xs border border-gray-100">
<h3 class="text-lg font-bold text-gray-900 mb-4">
Activity Chart
</h3>
<div class="h-48 bg-gray-50 rounded-xl
flex items-center justify-center">
<span class="text-gray-400">Chart placeholder</span>
</div>
</div>
</div>grid grid-cols-4. Four columns. col-span-2 stretches across two. col-span-4 takes the full width. Auto-placement fills left to right, top to bottom. No row positions needed.
But here's the thing people miss about grid vs flex: if your items need to be different sizes in a two-dimensional layout, grid is the only sane choice. Trying to fake a dashboard layout with flex and width percentages is a maintenance nightmare that breaks the moment someone adds a card. Grid just handles it.
Responsive Breakpoints
This is where most people get confused. Not the syntax — the syntax is dead simple. The confusion is about what "mobile-first" actually means when you're staring at a class list.
Unprefixed classes apply everywhere. sm: kicks in at 640px and above. md: at 768px. lg: at 1024px. xl: at 1280px. The key word: and above. A md: class doesn't turn off at lg:. It persists. So when you write grid-cols-1 md:grid-cols-2 lg:grid-cols-3, you're saying: one column by default (phones), two columns from 768px up (tablets), three columns from 1024px up (laptops). Each breakpoint overrides the one before it only because CSS specificity is equal and the later media query wins.
And this means your base styles — the ones without prefixes — are your mobile styles. Not your desktop styles. Build for small screens first, then add wider layouts. Desktop-first design leads to cramming things into mobile after the fact, and it always shows.
The sm breakpoint at 640px confuses people the most. It is not "small phones." It's phones turned sideways and small tablets. Phones in portrait mode get the unprefixed styles. A common mistake is designing for sm as if it were the mobile breakpoint, then wondering why the phone layout looks wrong.
<section class="px-4 py-12 sm:px-6 lg:px-8 max-w-7xl mx-auto">
<h2 class="text-2xl sm:text-3xl lg:text-4xl
font-bold text-center mb-8">
Our Services
</h2>
<!-- 1 col on mobile, 2 on sm, 3 on lg, 4 on xl -->
<div class="grid grid-cols-1 sm:grid-cols-2
lg:grid-cols-3 xl:grid-cols-4 gap-6">
<div class="bg-white p-6 rounded-xl shadow-xs
hover:shadow-md transition-shadow">
<h3 class="font-semibold text-lg mb-2">Design</h3>
<p class="text-gray-600 text-sm leading-relaxed">
Beautiful interfaces crafted with care.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-xs
hover:shadow-md transition-shadow">
<h3 class="font-semibold text-lg mb-2">Development</h3>
<p class="text-gray-600 text-sm leading-relaxed">
Clean, maintainable code that scales.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-xs
hover:shadow-md transition-shadow">
<h3 class="font-semibold text-lg mb-2">Marketing</h3>
<p class="text-gray-600 text-sm leading-relaxed">
Growth strategies that deliver results.
</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-xs
hover:shadow-md transition-shadow">
<h3 class="font-semibold text-lg mb-2">Support</h3>
<p class="text-gray-600 text-sm leading-relaxed">
24/7 support to keep you running.
</p>
</div>
</div>
</section>Read it left to right: one column by default, two at sm, three at lg, four at xl. Done.
The padding is just as important as the grid. px-4 sm:px-6 lg:px-8. Tighter on mobile to maximize content width. Wider on desktop so text doesn't slam into the edges. These details are invisible when done right and immediately noticeable when wrong.
Dark Mode Implementation
Prefix with dark:. That's the whole API.
Use a class-based strategy rather than the default. Out of the box, dark: follows the OS prefers-color-scheme setting, which sounds nice until a user wants manual control. In v4 you switch to a class with one line of CSS, @custom-variant dark (&:where(.dark, .dark *)); (shown in the configuration section below); in v3 it was darkMode: 'class' in the config file. The class strategy means you toggle a dark class on the HTML element and wire up a button. More work, but users get the choice. Worth it every time.
<!-- tailwind.config.js: darkMode: 'class' -->
<div class="bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700
rounded-2xl p-6 shadow-xs
transition-colors duration-200">
<div class="flex items-center gap-4 mb-4">
<div class="w-10 h-10 rounded-full
bg-blue-100 dark:bg-blue-900
flex items-center justify-center">
<span class="text-blue-600 dark:text-blue-300
font-bold">M</span>
</div>
<div>
<h3 class="font-semibold text-gray-900
dark:text-white">Marcus R.</h3>
<p class="text-sm text-gray-500
dark:text-gray-400">Frontend Developer</p>
</div>
</div>
<p class="text-gray-600 dark:text-gray-300
leading-relaxed">
Building interfaces people love to use.
Tailwind CSS makes it fast and enjoyable.
</p>
<button class="mt-4 px-4 py-2 rounded-lg
bg-blue-600 hover:bg-blue-700
dark:bg-blue-500 dark:hover:bg-blue-400
text-white text-sm font-medium
transition-colors">
View Profile
</button>
</div>Keep the light/dark pairs on the same line or adjacent lines. bg-white dark:bg-gray-800. When scanning markup, the relationship should be obvious at a glance.
Add transition-colors. Without it, the toggle produces an abrupt flash that looks broken. One class. Huge difference in perceived quality.
Custom Configuration and Theming
Real projects need custom colors and fonts. In Tailwind v4 that configuration lives in your CSS file. You import Tailwind, declare design tokens as CSS variables inside @theme, and each variable generates matching utilities:
@import "tailwindcss";
/* Class-based dark mode: dark: utilities apply under <html class="dark"> */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-brand-50: #eff6ff;
--color-brand-100: #dbeafe;
--color-brand-500: #3b82f6;
--color-brand-600: #2563eb;
--color-brand-700: #1d4ed8;
--color-brand-900: #1e3a5f;
--color-surface-light: #f8fafc;
--color-surface-dark: #0f172a;
--font-sans: Inter, system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--breakpoint-3xl: 120rem;
}
On Tailwind v3 the same setup is a JavaScript file. v4 still supports one for backward compatibility, but it is no longer detected automatically: you have to load it from your CSS with @config "../tailwind.config.js";. Useful during a migration, not needed for a new project.
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
900: '#1e3a5f',
},
surface: {
light: '#f8fafc',
dark: '#0f172a',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
screens: {
'3xl': '1920px',
},
borderRadius: {
'4xl': '2rem',
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
},
},
plugins: [],
}In the v3 file the critical thing is extend. We're inside theme.extend, not theme. Put colors directly under theme instead and you replace every default color Tailwind ships, so bg-blue-500 quietly stops working. v4 flips the default: variables in @theme add to the built-in theme, and you only replace a whole namespace when you ask for it with --color-*: initial;.
Either version gives you bg-brand-500, text-brand-900, bg-surface-light, font-mono and 3xl:grid-cols-6. rounded-4xl and p-18 need the extra v3 entries, but work in v4 without any config: v4 ships a 4xl radius and derives spacing utilities from one base value. v4 also renamed a few utilities. v3's shadow-sm is shadow-xs in v4 and bg-gradient-to-r is bg-linear-to-r; the examples in this article use the v4 names. Design tokens in one place. Brand color changes — you update one line.
v4 finds your template files automatically (it skips anything in .gitignore), so there is no content array to maintain. If a class used in a file outside your project folder, such as a component library in node_modules, isn't being generated, register that path with @source in your CSS. On v3, the content array in the config is the first place to check.
Building Reusable Component Patterns
Three patterns that combine everything above. Navbar, hero, footer. These are the responsive patterns you'll copy and modify for every project, so it's worth understanding what each breakpoint prefix is doing.
Responsive Navbar
<nav class="bg-white dark:bg-gray-900
border-b border-gray-200 dark:border-gray-800
sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Logo -->
<a href="/" class="text-xl font-bold text-gray-900
dark:text-white">
MyApp
</a>
<!-- Desktop links (hidden on mobile) -->
<div class="hidden md:flex items-center gap-8">
<a href="#" class="text-gray-600 dark:text-gray-300
hover:text-gray-900
dark:hover:text-white
text-sm font-medium
transition-colors">Features</a>
<a href="#" class="text-gray-600 dark:text-gray-300
hover:text-gray-900
dark:hover:text-white
text-sm font-medium
transition-colors">Pricing</a>
<a href="#" class="text-gray-600 dark:text-gray-300
hover:text-gray-900
dark:hover:text-white
text-sm font-medium
transition-colors">Docs</a>
<a href="#" class="px-4 py-2 bg-blue-600
hover:bg-blue-700 text-white
text-sm font-medium rounded-lg
transition-colors">Sign Up</a>
</div>
<!-- Mobile menu button (hidden on desktop) -->
<button type="button" id="menu-button"
class="md:hidden p-2 text-gray-600
dark:text-gray-300"
aria-label="Open main menu"
aria-expanded="false"
aria-controls="mobile-menu">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile menu panel, shown when the button is pressed -->
<div id="mobile-menu" class="hidden md:hidden px-4 pb-4 space-y-2">
<a href="#" class="block py-2 text-gray-600 dark:text-gray-300">Features</a>
<a href="#" class="block py-2 text-gray-600 dark:text-gray-300">Pricing</a>
<a href="#" class="block py-2 text-gray-600 dark:text-gray-300">Docs</a>
<a href="#" class="block py-2 font-medium text-blue-600">Sign Up</a>
</div>
</nav>
<script>
const menuButton = document.getElementById('menu-button');
const mobileMenu = document.getElementById('mobile-menu');
menuButton.addEventListener('click', () => {
const isOpen = menuButton.getAttribute('aria-expanded') === 'true';
menuButton.setAttribute('aria-expanded', String(!isOpen));
menuButton.setAttribute('aria-label', isOpen ? 'Open main menu' : 'Close main menu');
mobileMenu.classList.toggle('hidden', isOpen);
});
</script>
hidden md:flex. That pair is the entire responsive nav pattern. Below 768px the links vanish. At md and above they appear as a flex row. Hamburger does the opposite: visible by default, md:hidden on wider screens. An icon-only button needs an accessible name, so it gets aria-label. aria-expanded and aria-controls tell screen reader users whether the menu panel is open and which element it controls, and the few lines of script keep those attributes in sync as the panel toggles. Every responsive nav in Tailwind is some variation of this.
Hero Section
Stacks on mobile. Side-by-side on desktop.
<section class="bg-surface-light dark:bg-surface-dark">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8
py-16 sm:py-24 lg:py-32">
<div class="grid grid-cols-1 lg:grid-cols-2
gap-12 lg:gap-16 items-center">
<!-- Text content -->
<div class="text-center lg:text-left">
<span class="inline-block px-4 py-1.5
bg-blue-100 dark:bg-blue-900
text-blue-700 dark:text-blue-300
text-sm font-semibold rounded-full
mb-6">
New in v4.0
</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl
font-extrabold text-gray-900
dark:text-white leading-tight
tracking-tight">
Build faster with
<span class="text-transparent bg-clip-text
bg-linear-to-r from-blue-600
to-purple-600">
modern tools
</span>
</h1>
<p class="mt-6 text-lg text-gray-600
dark:text-gray-400 max-w-lg
mx-auto lg:mx-0">
Ship production-ready applications in
half the time. Our tools handle the
complexity so you can focus on building.
</p>
<div class="mt-8 flex flex-col sm:flex-row
gap-4 justify-center lg:justify-start">
<a href="#" class="px-8 py-3 bg-blue-600
hover:bg-blue-700 text-white
font-semibold rounded-xl
transition-colors text-center">
Get Started Free
</a>
<a href="#" class="px-8 py-3 border-2
border-gray-300
dark:border-gray-600
text-gray-700
dark:text-gray-300
font-semibold rounded-xl
hover:border-gray-400
transition-colors
text-center">
View Demo
</a>
</div>
</div>
<!-- Visual placeholder -->
<div class="bg-linear-to-br from-blue-500
to-purple-600 rounded-3xl
aspect-square max-w-md mx-auto
lg:max-w-none shadow-2xl"></div>
</div>
</div>
</section>Count the responsive layers: heading scales (text-4xl sm:text-5xl lg:text-6xl), grid switches from one to two columns at lg, text alignment shifts from centered to left at lg, buttons stack vertically then go horizontal at sm. Four separate responsive behaviors, all declared inline, all mobile-first. No media queries in a stylesheet somewhere.
The gradient text — text-transparent bg-clip-text bg-linear-to-r from-blue-600 to-purple-600 (bg-gradient-to-r in v3) — requires both text-transparent and bg-clip-text. Miss either one and you get a colored rectangle behind normal text. Not obvious.
Footer Component
Footers are trickier than they look. Four sections of content that need to reflow from stacked on mobile to a four-column grid on desktop, plus a bottom bar that changes direction.
<footer class="bg-gray-50 dark:bg-gray-900
border-t border-gray-200
dark:border-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6
lg:px-8 py-12">
<div class="grid grid-cols-2 md:grid-cols-4
gap-8 mb-8">
<!-- Brand column spanning full width on mobile -->
<div class="col-span-2 md:col-span-1">
<h3 class="text-lg font-bold text-gray-900
dark:text-white mb-4">MyApp</h3>
<p class="text-sm text-gray-500
dark:text-gray-400
leading-relaxed">
Building tools that developers
love since 2024.
</p>
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900
dark:text-white uppercase
tracking-wider mb-4">Product</h4>
<ul class="space-y-3">
<li><a href="#" class="text-sm text-gray-500
dark:text-gray-400
hover:text-gray-900
dark:hover:text-white
transition-colors">Features</a></li>
<li><a href="#" class="text-sm text-gray-500
dark:text-gray-400
hover:text-gray-900
dark:hover:text-white
transition-colors">Pricing</a></li>
<li><a href="#" class="text-sm text-gray-500
dark:text-gray-400
hover:text-gray-900
dark:hover:text-white
transition-colors">Changelog</a></li>
</ul>
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900
dark:text-white uppercase
tracking-wider mb-4">Company</h4>
<ul class="space-y-3">
<li><a href="#" class="text-sm text-gray-500
dark:text-gray-400
hover:text-gray-900
dark:hover:text-white
transition-colors">About</a></li>
<li><a href="#" class="text-sm text-gray-500
dark:text-gray-400
hover:text-gray-900
dark:hover:text-white
transition-colors">Blog</a></li>
<li><a href="#" class="text-sm text-gray-500
dark:text-gray-400
hover:text-gray-900
dark:hover:text-white
transition-colors">Careers</a></li>
</ul>
</div>
</div>
<!-- Bottom bar -->
<div class="border-t border-gray-200
dark:border-gray-800 pt-8
flex flex-col sm:flex-row
items-center justify-between gap-4">
<p class="text-sm text-gray-500
dark:text-gray-400">
© 2026 MyApp. All rights reserved.
</p>
<div class="flex gap-6">
<a href="#" class="text-gray-400
hover:text-gray-600
dark:hover:text-gray-300
transition-colors">
Twitter
</a>
<a href="#" class="text-gray-400
hover:text-gray-600
dark:hover:text-gray-300
transition-colors">
GitHub
</a>
</div>
</div>
</div>
</footer>
Two columns on mobile. Brand section takes a full row via col-span-2, nav columns sit side by side below. At md it expands to four columns and everything reflows naturally. Bottom bar: flex-col sm:flex-row. Stacked on phones, horizontal on wider screens.
And space-y-3 on the link lists? Vertical spacing between items without individual margin classes. Tiny thing. Saves real time across a full project.
Wrapping Up
Don't customize your spacing scale. Don't customize your color palette until after launch. Don't add custom breakpoints. Tailwind's defaults are a sensible starting point — use them until you have a specific reason not to.
The urge to customize everything on day one is strong. Resist it. The default spacing scale works. The default breakpoints cover common device widths. The default color palette is broad enough for most designs, but it does not guarantee accessible contrast for you: light grays such as text-gray-400 on white fall below the WCAG AA ratio for body text, so check your text and background pairs. Every hour you spend tweaking your theme before you've built anything is an hour wasted. Ship first. Customize when the design demands it, not when your instincts tell you to "make it yours."