Skip to main content

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:

  1. Colocation โ€” Styles live with the component, not in a separate file. When you delete a component, its styles are gone too. Zero dead CSS.

  2. 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.

  3. Speed โ€” Once you learn the utility names, you style faster than writing custom CSS. No context switching between files.

  4. 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.

  5. No specificity issues โ€” Every utility has the same specificity. No !important wars.

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-3 for a responsive stats row
  • Task list: space-y-2 for vertical spacing, hover:shadow-md + transition-shadow on each Link
  • Status badges: Dynamic classes with a statusStyles map (same pattern as priorityStyles in 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-4 on the form, space-y-1.5 on 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โ€‹

  1. Custom utility patterns: Create a reusable set of Tailwind classes for badges. Define a badgeVariants object 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;
  2. 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>
  3. 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โ€‹


Next up: Chapter 10 โ€” Advanced Tailwind โ†’

Custom themes, dark mode, animations, and advanced patterns to make TaskFlow look polished and professional.