Mastering Tailwind CSS for Modern Web Design
Discover advanced Tailwind CSS techniques to create beautiful and responsive web designs.

Tailwind CSS flips the traditional CSS mental model on its head. Instead of writing semantic class names that map to custom styles, you compose small, single-purpose utility classes directly in your HTML. The result is a design system with almost no custom CSS — and a codebase that a new developer can read and change without fear of unexpected side effects. This guide covers the fundamentals and the techniques that will make you genuinely fast with Tailwind.
Why Utility-First?
The core promise: you never leave your markup to style it.
Traditional approach:
<!-- HTML -->
<button class="btn btn-primary">Save</button>
/* CSS — somewhere else */ .btn { padding: 0.5rem 1rem; border-radius: 0.375rem;
font-weight: 600; } .btn-primary { background: #6366f1; color: white; }
.btn-primary:hover { background: #4f46e5; }
Tailwind approach:
<button
class="rounded-md bg-indigo-500 px-4 py-2 font-semibold text-white hover:bg-indigo-600"
>
Save
</button>
The style lives next to the HTML. No context switching, no naming gymnastics, no cascading side effects. The trade-off is verbose class strings — a cost most teams find manageable once they learn the naming system.
Installation
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Configure content sources in tailwind.config.ts:
import type { Config } from "tailwindcss";
export default {
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./content/**/*.mdx",
],
theme: {
extend: {},
},
plugins: [],
} satisfies Config;
Add to your global CSS:
@tailwind base;
@tailwind components;
@tailwind utilities;
Tailwind scans the files in content and only ships the classes you actually use. A production bundle is typically 5–15 kB.
Core Concepts
Spacing
Tailwind’s spacing scale is based on a 4px unit. p-1 = 4px, p-4 = 16px, p-8 = 32px.
<div class="m-4 space-y-4 p-6">
<!-- m-4 = margin: 16px -->
<!-- p-6 = padding: 24px -->
<!-- space-y-4 = 16px gap between children -->
</div>
Typography
<h1 class="text-4xl font-bold tracking-tight text-gray-900">Heading One</h1>
<p class="text-base leading-7 text-gray-600">
Body text with comfortable line height.
</p>
<span class="text-sm font-medium tracking-widest text-indigo-600 uppercase">
Label
</span>
Flexbox and Grid
<!-- Horizontal navbar -->
<nav class="flex items-center justify-between px-6 py-4">
<Logo />
<div class="flex items-center gap-4">
<a href="/blog">Blog</a>
<a href="/about">About</a>
</div>
</nav>
<!-- 3-column grid, 1-column on mobile -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{cards.map(card => <Card key="{card.id}" {...card} />)}
</div>
Responsive Design
Tailwind uses mobile-first breakpoint prefixes. A class without a prefix applies at all sizes; a prefixed class overrides at that breakpoint and above.
| Prefix | Breakpoint |
|---|---|
| (none) | All sizes |
sm: |
≥ 640px |
md: |
≥ 768px |
lg: |
≥ 1024px |
xl: |
≥ 1280px |
2xl: |
≥ 1536px |
<!-- Stack on mobile, side-by-side on desktop -->
<div class="flex flex-col gap-8 lg:flex-row lg:items-start">
<aside class="w-full lg:w-64 lg:shrink-0">…</aside>
<main class="min-w-0 flex-1">…</main>
</div>
<!-- Font size scales up with viewport -->
<h1 class="text-2xl font-bold sm:text-3xl lg:text-5xl">Responsive Heading</h1>
State Variants
Apply styles conditionally based on element state:
<!-- Hover, focus, active -->
<button
class="bg-indigo-500 hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:scale-95"
>
Click me
</button>
<!-- Form state -->
<input
class="border border-gray-300 invalid:border-red-500 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
/>
<!-- Disabled -->
<button class="bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50">
Submit
</button>
<!-- Parent state with group -->
<div class="group rounded-lg border p-4 hover:border-indigo-500">
<h3 class="font-semibold group-hover:text-indigo-600">Card Title</h3>
<p class="text-gray-500 group-hover:text-gray-700">Card body</p>
</div>
Dark Mode
Enable dark mode in your config:
export default {
darkMode: "class", // toggles via a "dark" class on <html>
// or "media" to follow the OS preference automatically
} satisfies Config;
Use dark: variants in markup:
<body class="bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-100">
<div class="rounded-lg bg-white shadow dark:bg-gray-800">
<p class="text-gray-600 dark:text-gray-300">Card content</p>
</div>
</body>
Toggle in JavaScript:
document.documentElement.classList.toggle("dark");
Custom Configuration
Extend Tailwind’s defaults with your brand colours, fonts, and spacing:
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
500: "#3b82f6",
900: "#1e3a8a",
},
},
fontFamily: {
sans: ["Inter", "ui-sans-serif", "system-ui"],
mono: ["JetBrains Mono", "ui-monospace"],
},
spacing: {
18: "4.5rem",
112: "28rem",
},
borderRadius: {
"4xl": "2rem",
},
},
},
} satisfies Config;
Custom values are available as first-class utilities: bg-brand-500, font-mono, p-18.
Arbitrary Values
Need a one-off value not in the scale? Use square brackets:
<div class="top-[117px] w-[calc(100%-2rem)] bg-[#1a1a2e]">
Arbitrary positioning
</div>
<p class="text-[13px] leading-[1.6]">Fine-tuned typography</p>
Lean on these sparingly — overuse signals the design token is missing from your config.
Component Patterns with @apply
For repeated patterns across many files, extract them with @apply in CSS:
/* globals.css */
@layer components {
.btn {
@apply inline-flex items-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none;
}
.btn-primary {
@apply btn bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-500;
}
.btn-ghost {
@apply btn bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500;
}
}
Use sparingly. Heavy @apply use recreates the problem Tailwind solves. Prefer React components for reuse over CSS abstractions.
Plugins
The official Tailwind plugins add common patterns:
npm install -D @tailwindcss/typography @tailwindcss/forms @tailwindcss/aspect-ratio
import typography from "@tailwindcss/typography";
import forms from "@tailwindcss/forms";
export default {
plugins: [typography, forms],
} satisfies Config;
The typography plugin provides prose classes for rendering rich markdown/HTML content beautifully:
<article class="prose prose-lg prose-indigo dark:prose-invert max-w-none">
{/* markdown rendered to HTML */}
</article>

Useful VS Code Workflow
Install the Tailwind CSS IntelliSense extension. You get:
- Autocomplete for class names.
- Live CSS preview on hover.
- Lint warnings for unknown classes.
- Sorting with the
prettier-plugin-tailwindcss(sorts classes in a consistent, canonical order):
npm install -D prettier prettier-plugin-tailwindcss
// .prettierrc
{ "plugins": ["prettier-plugin-tailwindcss"] }
Sorted classes are easier to scan for duplicates and easier to diff in pull requests.
Tips for Real Projects
Consistent sizing — decide early whether you use size-* (sets both width and height) or separate w-*/h-*. Tailwind 3.3+ has size-* — use it for icons and avatars.
Avoid !important overrides — if you find yourself reaching for !, something is wrong in the design token structure.
Component variants without a library — use cva (class-variance-authority) to express variants cleanly:
import { cva } from "class-variance-authority";
const button = cva("rounded-md font-semibold transition-colors", {
variants: {
intent: {
primary: "bg-indigo-600 text-white hover:bg-indigo-700",
ghost: "bg-transparent text-gray-700 hover:bg-gray-100",
},
size: {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
},
},
defaultVariants: { intent: "primary", size: "md" },
});
// <Button intent="ghost" size="sm">Cancel</Button>
Closing Thoughts
Tailwind’s learning curve is front-loaded — the class names feel foreign for the first few hours, then click into place. Once they do, the speed is real: you style without leaving your component, redesigns happen in one file, and dead CSS is impossible because styles live with the markup. Invest a day in the config and the utility naming convention; the return comes back quickly.
About the author
The Plake Team
Writes long-form essays on craft, software, and the slow web. Curates the Sunday digest read by 88,000+ readers.
Read moreKeep reading
Related Articles






