Chapter 9: Tailwind CSS Fundamentals
You've been writing CSS the traditional way โ class names, separate files, specificity battles. Tailwind CSS flips the model: instead of writing custom CSS, you compose pre-built utility classes directly in your markup. It sounds weird. Then it clicks, and you never go back.
๐ Where we are: TaskFlow is fully functional โ routing (Ch 7), validated forms (Ch 8), custom hooks, context. But it looks like a 1995 website. This chapter (and Ch 10) fix that. By the end, TaskFlow will have a modern, responsive design.
๐ง Conceptsโ
1. Why Utility-First CSS?โ
Let's compare the approaches you've probably seen:
Traditional CSS โ Custom class names, separate files:
/* styles.css */
.task-card {
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.task-card:hover {
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
<div className="task-card">...</div>
Problems: naming things is hard, styles drift from markup, dead CSS accumulates, specificity wars.
CSS Modules โ Scoped styles per component:
import styles from "./TaskCard.module.css";
<div className={styles.card}>...</div>
Better scoping, but still separate files and naming.
Styled Components / CSS-in-JS โ Styles in JavaScript:
const Card = styled.div`
padding: 1rem;
border-radius: 0.5rem;
`;
Runtime cost, bundle bloat, SSR complexity.
Tailwind CSS โ Utility classes directly in markup:
<div className="p-4 rounded-lg border border-gray-200 shadow-sm hover:shadow-md">
...
</div>
No naming. No separate files. No dead CSS. What you see is what you get.
2. The Mental Shiftโ
The initial reaction to Tailwind is always: "That looks ugly! The HTML is full of classes!"
Here's why it works:
-
Colocation โ Styles live with the component, not in a separate file. When you delete a component, its styles are gone too. Zero dead CSS.
-
Consistency โ Tailwind uses a design system (spacing scale, color palette, etc.). You can't accidentally use
padding: 13pxโ you pick from a constrained set of values. -
Speed โ Once you learn the utility names, you style faster than writing custom CSS. No context switching between files.
-
Tiny bundles โ Tailwind's compiler scans your code and only includes the utilities you actually use. A full Tailwind build is typically 8-15KB gzipped.
-
No specificity issues โ Every utility has the same specificity. No
!importantwars.
3. How Tailwind Works Under the Hoodโ
Tailwind is a build-time tool, not a runtime library:
Your JSX files โ Tailwind scans for class names โ Generates only the CSS you use โ Tiny CSS output
Write className="p-4 text-blue-500" โ Tailwind generates:
.p-4 { padding: 1rem; }
.text-blue-500 { color: rgb(59 130 246); }
Nothing else. If you never use text-red-500, it's not in your CSS.
4. The Spacing Scaleโ
Tailwind's spacing system is based on a 0.25rem (4px) unit scale. Each number = that many 4px units:
p-1โ 4px,p-2โ 8px,p-4โ 16px,p-8โ 32px
The same scale applies to margin (m-), gap, width, height, and more. You'll internalize the common ones (p-2, p-4, p-6, p-8) quickly โ for the full list, see the Tailwind spacing docs.
Arbitrary values: Need a specific value not in the scale? Use brackets: p-[13px], w-[calc(100%-2rem)].
5. Responsive Designโ
Tailwind is mobile-first. Unprefixed utilities apply to all screen sizes. Breakpoint prefixes apply at that size and up:
sm:(640px) โmd:(768px) โlg:(1024px) โxl:(1280px) โ2xl:(1536px)
Full breakpoint reference: Tailwind responsive docs.
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 1 column on mobile, 2 on tablet, 3 on desktop */}
</div>
Think mobile-first: Start with the mobile layout, then add breakpoints for larger screens.
6. State Modifiersโ
Apply styles on specific states:
<button className="
bg-blue-500
hover:bg-blue-600 /* mouse hover */
focus:ring-2 /* keyboard focus */
focus:ring-blue-400
active:bg-blue-700 /* while clicking */
disabled:opacity-50 /* disabled state */
disabled:cursor-not-allowed
">
Save
</button>
Group and Peer โ Style children based on parent or sibling state:
{/* Group: style children when parent is hovered */}
<div className="group cursor-pointer">
<h3 className="group-hover:text-blue-500">Task Title</h3>
<p className="group-hover:underline">Click to view</p>
</div>
{/* Peer: style element based on sibling state */}
<input className="peer" type="text" placeholder="Search..." />
<p className="hidden peer-focus:block text-sm text-gray-500">
Type to search tasks
</p>
๐ก Examplesโ
Installing Tailwind v4 with Viteโ
Tailwind CSS v4 (current) uses a new setup โ it's simpler than v3:
npm install tailwindcss @tailwindcss/vite
Add the plugin to vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});
Replace the contents of your src/index.css:
@import "tailwindcss";
That's it. No tailwind.config.js needed for basic usage (v4 auto-detects content).
Common Patterns at a Glanceโ
You'll pick up utility names fast โ they map directly to CSS properties. Here are the categories you'll use most: typography (text-sm, font-bold, text-gray-500), spacing (p-4, m-2, space-y-4), borders (border, rounded-lg, shadow-sm), and colors (bg-blue-500, text-white). Browse the full list at tailwindcss.com/docs.
Flexbox & Gridโ
{/* Flexbox: horizontal bar with space between */}
<div className="flex items-center justify-between p-4">
<h2 className="text-lg font-semibold">Tasks</h2>
<button className="px-3 py-1.5 bg-blue-500 text-white rounded-md">+ Add Task</button>
</div>
{/* Grid: responsive columns */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-lg shadow-sm">Card 1</div>
<div className="bg-white p-4 rounded-lg shadow-sm">Card 2</div>
<div className="bg-white p-4 rounded-lg shadow-sm">Card 3</div>
</div>
Flexbox docs: tailwindcss.com/docs/flex ยท Grid docs: tailwindcss.com/docs/grid-template-columns
A Complete Card Component in Tailwindโ
interface TaskCardProps {
title: string;
description?: string;
priority: "low" | "medium" | "high";
status: string;
}
const priorityStyles = {
low: "text-green-700 bg-green-50 border-green-200",
medium: "text-yellow-700 bg-yellow-50 border-yellow-200",
high: "text-red-700 bg-red-50 border-red-200",
};
export default function TaskCard({
title,
description,
priority,
status,
}: TaskCardProps) {
return (
<div className="group rounded-lg border border-gray-200 bg-white p-4
shadow-sm transition-shadow hover:shadow-md">
<div className="flex items-start justify-between">
<h3 className="font-medium text-gray-900 group-hover:text-blue-600
transition-colors">
{title}
</h3>
<span
className={`inline-flex items-center rounded-full border px-2 py-0.5
text-xs font-medium ${priorityStyles[priority]}`}
>
{priority}
</span>
</div>
{description && (
<p className="mt-2 text-sm text-gray-500 line-clamp-2">
{description}
</p>
)}
<div className="mt-3 flex items-center justify-between">
<span className="text-xs text-gray-400">{status}</span>
<button className="text-xs text-gray-400 opacity-0 transition-opacity
group-hover:opacity-100 hover:text-blue-500">
View โ
</button>
</div>
</div>
);
}
Notice: group on the parent + group-hover: on children. The "View โ" link only appears when you hover the card. No JavaScript needed.
๐จ Project Task: Rebuild TaskFlow with Tailwindโ
Time for a visual overhaul. We're ripping out all custom CSS and rebuilding with Tailwind utilities.
Step 1: Install Tailwindโ
npm install tailwindcss @tailwindcss/vite
Update vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
});
Replace src/index.css:
@import "tailwindcss";
Delete any component-specific .css files.
Step 2: Rebuild the Layoutโ
src/components/Layout.tsx
import { Outlet, NavLink } from "react-router-dom";
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
`flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
isActive
? "bg-gray-100 text-gray-900"
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
}`;
const Layout = () => {
return (
<div className="flex h-screen bg-gray-50">
{/* Sidebar */}
<aside className="hidden w-64 flex-col border-r border-gray-200 bg-white p-4 md:flex">
<div className="mb-8">
<h1 className="text-xl font-bold text-gray-900">๐ TaskFlow</h1>
</div>
<nav className="flex flex-col gap-1">
<NavLink to="/" end className={navLinkClass}>
๐ Dashboard
</NavLink>
<NavLink to="/settings" className={navLinkClass}>
โ๏ธ Settings
</NavLink>
</nav>
<div className="mt-auto pt-4 border-t border-gray-200">
<p className="text-xs text-gray-400">TaskFlow v0.9.0</p>
</div>
</aside>
{/* Main content */}
<main className="flex-1 overflow-y-auto p-4 md:p-8">
<Outlet />
</main>
</div>
);
}
Step 3: Rebuild the Dashboardโ
Apply the same Tailwind patterns to src/pages/Dashboard.tsx:
- Summary cards: Use
grid grid-cols-1 gap-4 sm:grid-cols-3for a responsive stats row - Task list:
space-y-2for vertical spacing,hover:shadow-md+transition-shadowon each Link - Status badges: Dynamic classes with a
statusStylesmap (same pattern aspriorityStylesin the card example above) - Empty state:
py-8 text-center text-gray-400
The full code follows the same patterns as the Layout โ you have all the building blocks now.
Step 4: Style the TaskFormโ
Key patterns for form styling:
- Labels:
text-sm font-medium text-gray-700 - Inputs:
w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 - Error messages:
text-sm text-red-600 - Submit button:
w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed - Form spacing:
space-y-4on the form,space-y-1.5on each field group
Apply this pattern to every field in your TaskForm โ label, input, error message.
Step 5: Responsive Sidebarโ
The sidebar should collapse on mobile. Add a mobile header:
// In Layout.tsx โ add above <main>:
<div className="flex items-center justify-between border-b border-gray-200
bg-white p-4 md:hidden">
<h1 className="text-lg font-bold">๐ TaskFlow</h1>
<button className="rounded-md p-2 text-gray-600 hover:bg-gray-100">
{/* Hamburger icon โ we'll replace with a proper icon later */}
<span className="text-xl">โฐ</span>
</button>
</div>
The sidebar uses hidden md:flex โ invisible on mobile, visible from md breakpoint up.
Step 6: Verifyโ
- All custom CSS files are deleted
- Layout renders correctly โ sidebar on desktop, hidden on mobile
- Cards have proper padding, borders, and shadows
- Hover states work (cards lift, nav items highlight)
- Text is readable and properly sized
- Form inputs have focus rings
- Responsive: stack to single column on mobile, multi-column on desktop
- No visual regressions from the CSS rewrite
๐งช Challengeโ
-
Custom utility patterns: Create a reusable set of Tailwind classes for badges. Define a
badgeVariantsobject and use it throughout the app:const badgeVariants = {
todo: "bg-gray-100 text-gray-700",
"in-progress": "bg-blue-50 text-blue-700",
done: "bg-green-50 text-green-700",
} as const; -
Skeleton loading: Build a skeleton loader using Tailwind's
animate-pulse:<div className="animate-pulse space-y-3">
<div className="h-4 w-3/4 rounded bg-gray-200" />
<div className="h-4 w-1/2 rounded bg-gray-200" />
</div> -
Responsive detail page: Make the TaskDetail page show the sidebar info below the main content on mobile, and beside it on desktop using
flex-col md:flex-row.
๐ Further Readingโ
- Tailwind CSS Documentation โ the official docs (excellent search!)
- Tailwind CSS v4 Blog Post โ what's new in v4
- Tailwind Play โ live playground to experiment
- Refactoring UI โ the design book by Tailwind's creators
- Why Tailwind (by its creator) โ the philosophy explained
Next up: Chapter 10 โ Advanced Tailwind โ
Custom themes, dark mode, animations, and advanced patterns to make TaskFlow look polished and professional.