Section 4 · Shipping a Project
Styling with Tailwind
17 min3 min readFill in the blanks
Lesson at a glance
Add Tailwind CSS v4 to the Vite project with its official plugin, style the Watchlist with utility classes and state variants, and customise the theme in CSS.
- 1Install it
- 2Reading utility classes
- 3Variants: conditions in a prefix
- 4Your own design tokens
- 5The trade-off
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
The Watchlist works, and it looks like 1998. You could write a stylesheet, and plenty of excellent apps do. This course uses Tailwind CSS, because it’s extremely common in React codebases you’ll join, and because styling right in your JSX keeps components self-contained: a MovieItem file holds its markup, its behaviour and its look.
Tailwind is a large set of small utility classes, each doing one thing: p-4 adds padding, rounded-lg rounds corners, text-slate-600 sets a color. You compose them in className. Tailwind scans your files, finds the classes you used, and generates a stylesheet containing only those.
Install it
Tailwind v4 has an official Vite plugin, and setup is three steps. Install the packages:
npm install tailwindcss @tailwindcss/viteAdd the plugin to your Vite config:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});And import Tailwind at the top of your main stylesheet, which main.jsx already imports:
@import "tailwindcss";That’s all. There’s no tailwind.config.js and no PostCSS config in v4 by default. Tailwind finds your source files on its own. Vite restarts the dev server by itself when you save vite.config.js; if the styles still don’t appear, stop it and run npm run dev again.
Reading utility classes
Here’s the Watchlist shell:
<main className="mx-auto max-w-xl px-4 py-10">
<h1 className="text-3xl font-bold tracking-tight text-slate-900">Watchlist</h1>
<p className="mt-1 text-sm text-slate-500">{left} to watch</p>
{/* …form, filters, list */}
</main>The names are terse but systematic, and after a day you’ll read them as fast as CSS:
- Spacing uses a scale where each step is 0.25rem:
p-4is 1rem of padding,mt-1is 0.25rem of top margin,px-4is horizontal padding.mx-autocentres a block. - Sizes and type:
max-w-xlcaps the width,text-3xlsets the font size,font-boldthe weight. - Colors come from a palette with shades 50 to 950:
text-slate-500,bg-indigo-600.
Your editor can help: the official Tailwind CSS IntelliSense extension for VS Code autocompletes class names and shows the CSS each one produces on hover.
Variants: conditions in a prefix
Prefix a class to apply it only under a condition:
<li className="flex items-center gap-3 rounded-lg border border-slate-200 bg-white p-3 dark:border-slate-700 dark:bg-slate-800">
<span className={movie.watched ? 'flex-1 text-slate-400 line-through' : 'flex-1 text-slate-900 dark:text-slate-100'}>
{movie.title}
</span>
<button
aria-pressed={movie.watched}
onClick={() => onToggle(movie.id)}
className="rounded-md border border-slate-300 px-3 py-1 text-sm hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-indigo-500 aria-pressed:border-indigo-600 aria-pressed:bg-indigo-600 aria-pressed:text-white"
>
{movie.watched ? 'Watched' : 'Mark watched'}
</button>
</li>hover:andfocus-visible:handle interaction states. Always give interactive elements a visible focus style for keyboard users.dark:applies when the user’s system is in dark mode (it followsprefers-color-schemeby default).aria-pressed:applies when the element’saria-pressedis true. The toggle’s look follows the same attribute screen readers announce, so the two can’t disagree.- Breakpoints are mobile-first:
sm:,md:andlg:apply from that width up, so write the phone layout first and add prefixes for larger screens.
Conditions that depend on React state, like movie.watched on the span, are a plain ternary choosing between two complete class strings.
Your own design tokens
Customise the theme in CSS with @theme. Each variable becomes utilities automatically:
@import "tailwindcss";
@theme {
--color-brand-500: oklch(0.62 0.19 285);
--color-brand-600: oklch(0.55 0.21 285);
--font-display: "Inter", system-ui, sans-serif;
}Now bg-brand-600, text-brand-500 and font-display work like built-in classes. Define a handful of tokens for your brand color and fonts rather than scattering one-off values like bg-[#6d28d9] through your components.
The trade-off
Long class lists are the honest cost. A styled button can carry fifteen classes. The answer isn’t to hide them in CSS with @apply; it’s what you’ve practised all course: extract a component. A Button component holds those fifteen classes in one place, and everywhere else you write <Button>. Components are your unit of reuse in React, for styles as much as for behaviour.
Tailwind isn’t the only good answer. CSS Modules (built into Vite: name a file MovieItem.module.css and import it) give you scoped class names with ordinary CSS, and many teams prefer them. The skills transfer either way: semantic markup, a consistent spacing and color scale, visible focus states and a layout that works on a phone first.
The Watchlist looks like a real product now. One thing left: getting it onto the internet, which is the next lesson.
Key takeaways
- Tailwind v4 needs
npm install tailwindcss @tailwindcss/vite, the plugin in vite.config.js, and@import "tailwindcss";in your CSS; no config file is required. - Utility classes map to single CSS declarations on a shared scale, such as
p-4for 1rem of padding andtext-slate-600for a palette color. - Prefixes apply a class conditionally, for example
md:above a breakpoint,hover:on hover,dark:in dark mode andaria-pressed:when that attribute is true. - Tailwind finds classes by scanning your files as text, so always write complete class names and never build them with string interpolation.
- Customise colors, fonts and other tokens with CSS variables in an
@themeblock.