π¬οΈ Tailwind CSS Workflow and Setup
Tailwind flips CSS on its head: instead of writing custom stylesheets, you compose designs from tiny utility classes right in your markup. This lesson explains why that works, installs Tailwind the modern way, and gives you a build-and-watch workflow you can reuse on every project.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain the utility-first philosophy and how it differs from component frameworks like Bootstrap
- Install Tailwind CSS with the Vite plugin, the standalone CLI, or the play CDN, and know when to use each
- Describe how Tailwind's engine scans your source to generate only the CSS you use
- Handle dynamic class names safely so styles never go missing
- Set up an efficient watch-mode workflow with editor tooling for autocomplete and linting
Estimated Time: 35β45 minutes β’ Difficulty: Intermediate
Hands-on: Scaffold a working Tailwind + Vite project from an empty folder and see live rebuilds.
In This Lesson
The Utility-First Idea
Most CSS frameworks hand you finished components β a .card, a .btn, a .navbar β and you accept their look or fight to override it. Tailwind CSS takes the opposite approach: it gives you a large set of tiny, single-purpose utility classes like flex, pt-4, and text-center, and you assemble your own design by combining them directly in your HTML.
π‘ An analogy: Bootstrap is like buying pre-built furniture β fast, but everyone's living room looks the same. Tailwind is like a well-organized box of LEGO bricks: every piece is small and standard, yet you can build almost anything, and two projects rarely look alike.
Here is the same notification component written both ways. First the traditional approach, where structure and style live in separate files:
<!-- Traditional CSS: markup + a separate stylesheet -->
<div class="chat-notification">
<div class="chat-notification-logo"></div>
<div class="chat-notification-content">
<h4>ChitChat</h4>
<p>You have a new message!</p>
</div>
</div>
<style>
.chat-notification {
display: flex;
max-width: 24rem;
margin: 0 auto;
padding: 1.5rem;
border-radius: 0.5rem;
background-color: #fff;
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1),
0 10px 10px -5px rgb(0 0 0 / 0.04);
}
.chat-notification-logo {
flex-shrink: 0;
height: 3rem;
width: 3rem;
border-radius: 9999px;
background-color: #6366f1;
}
/* β¦and more rules for the content, heading, and textβ¦ */
</style>
Now the same result in Tailwind β no separate stylesheet, no invented class names to keep in sync:
<!-- Tailwind: the design lives in the markup -->
<div class="flex max-w-sm mx-auto p-6 bg-white rounded-lg shadow-xl">
<div class="shrink-0 size-12 rounded-full bg-indigo-500"></div>
<div class="ml-6 pt-1">
<h4 class="text-xl font-semibold text-gray-900">ChitChat</h4>
<p class="text-base text-gray-600">You have a new message!</p>
</div>
</div>
β Why developers reach for it
- No context switching β you style where you build, never hunting between HTML and CSS files.
- Built-in design constraints β the spacing, color, and type scales are pre-defined, so your UI stays visually consistent.
- Responsive by default β prefix any utility with
md:orlg:for breakpoint-specific styles. - Tiny production CSS β the build only includes the utilities you actually used, often just a few kilobytes.
β οΈ The honest trade-off
Utility classes make your markup longer and, at first glance, busier. The payoff is that you rarely write custom CSS, and when you delete a component you delete its styles with it β no orphaned stylesheet rules piling up over time. Later in this module you'll learn to extract repeated patterns into components so the markup stays readable.
Installing Tailwind (Three Paths)
Tailwind is a build-time tool: it reads your markup and produces a stylesheet. There are three common ways to wire it up, from best-for-real-apps to best-for-a-quick-sketch.
Path 1: The Vite plugin (recommended for apps)
If your project uses Vite β which powers most modern React, Vue, and Svelte setups β the dedicated plugin is the fastest and simplest option. Install the two packages:
npm install tailwindcss @tailwindcss/vite
Register the plugin in vite.config.js:
// vite.config.js
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [tailwindcss()],
});
Then create your main stylesheet with a single import and load it from your app's entry point:
/* src/style.css */
@import "tailwindcss";
π What changed in Tailwind v4
Tailwind CSS v4 (the current major version) replaced the old trio of @tailwind base; @tailwind components; @tailwind utilities; directives with one line: @import "tailwindcss";. It also detects your source files automatically, so a content array is no longer required in most projects. If you work on an older codebase you may still see a tailwind.config.js and the three @tailwind lines β that is the v3 style, and it still works.
Path 2: The standalone CLI (for static sites)
For plain HTML sites with no bundler, the CLI compiles your CSS directly. You do not even need Node modules if you use the standalone executable, but the npm route is most common:
# Install and run the CLI in watch mode
npm install tailwindcss @tailwindcss/cli
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch
Your input.css is again just the import, and you link the generated output.css in your page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/dist/output.css" rel="stylesheet">
</head>
<body>
<h1 class="text-3xl font-bold underline text-blue-600">Hello world!</h1>
</body>
</html>
Path 3: The play CDN (prototyping only)
For a quick experiment β a CodePen, a bug report, a five-minute sketch β you can pull Tailwind straight from a script tag with no build at all:
<script src="https://cdn.tailwindcss.com"></script>
β οΈ Never ship the CDN to production
The play CDN compiles Tailwind in the browser on every page load. That means a large download, no minification, no plugins, and a flash of unstyled content. It is a fantastic sketchpad and a terrible production strategy. For anything real, use Path 1 or Path 2.
How the Engine Works
Tailwind could theoretically generate millions of utility classes. It doesn't ship all of them β instead, its engine scans your source files, sees which classes you actually wrote, and generates only those. This on-demand generation is why a Tailwind build can end up smaller than hand-written CSS.
HTML Β· JSX Β· Vue] -->|scan for class names| B[Tailwind engine] B -->|generate only
used utilities| C[Optimized CSS] C -->|link in page| D[Browser]
The scan is a plain-text pattern match, not a JavaScript evaluation. Tailwind reads your files as strings and looks for things that look like class names. That single fact explains almost every "why is my style missing?" problem you will ever hit β which we tackle in the next section.
π‘ Arbitrary values
When the built-in scale doesn't have exactly what you need, square-bracket syntax lets you drop in a one-off value without leaving your markup: top-[117px], bg-[#1da1f2], or grid-cols-[1fr_500px_2fr]. The engine generates just that class, on demand. Reach for the scale first and arbitrary values only when you must.
Pointing the engine at your files
In Tailwind v4, source detection is automatic: it starts from your CSS file's location, walks your project, and ignores anything in .gitignore (like node_modules). If you need to include a file outside that net β say a component library in node_modules β add it explicitly with @source:
/* src/style.css */
@import "tailwindcss";
/* Also scan a dependency that ships Tailwind classes */
@source "../node_modules/@my-org/ui/dist";
On a v3 project the equivalent lives in the config file's content array, which you must maintain by hand:
// tailwind.config.js (v3 style β legacy projects)
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx,vue}",
],
theme: { extend: {} },
plugins: [],
};
Dynamic Class Names
Because the engine matches plain text, it can only find complete, literal class names in your source. String concatenation defeats it. This is the single most common Tailwind gotcha, so let's make it concrete.
// β BROKEN β the engine never sees "bg-blue-500" as a whole string
function Alert({ color }) {
return <div className={`bg-${color}-500 p-4`}>...</div>;
}
At build time the file only contains the fragments bg- and -500; the full class is assembled at runtime, long after Tailwind has finished scanning. The class is never generated, and the alert renders unstyled. There are two clean fixes.
Fix 1: Map to complete class names
Store the full class strings in a lookup so each one appears literally in your source. This is the preferred fix β it is explicit and needs no configuration:
// β
Each full class name appears literally in the file
const COLORS = {
info: 'bg-blue-500',
success: 'bg-green-500',
danger: 'bg-red-500',
};
function Alert({ color }) {
return <div className={`${COLORS[color]} p-4`}>...</div>;
}
Fix 2: Safelist classes you truly can't spell out
When class names genuinely come from data you don't control at build time, add them to a safelist so Tailwind always includes them. In v4 this is done in CSS with @source inline(...):
/* Force these classes into the build even if never found in source */
@import "tailwindcss";
@source inline("bg-red-500 bg-green-500 bg-blue-500");
The v3 config equivalent supports regex patterns and variants:
// tailwind.config.js (v3 style)
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
safelist: [
{
pattern: /bg-(red|green|blue)-(100|500|900)/,
variants: ['hover', 'focus'],
},
],
};
β οΈ Keep safelists tight
A safelist is an escape hatch, not a habit. Overly broad patterns (like /bg-.*/) drag thousands of unused classes back into your bundle β undoing the very optimization that makes Tailwind fast. Prefer Fix 1 whenever you can.
An Efficient Workflow
A good Tailwind setup rebuilds instantly as you type and catches mistakes before they reach the browser. Two ingredients make that happen: watch mode and editor tooling.
Watch mode as an npm script
Whichever install path you chose, wrap the build command in your package.json so teammates run one memorable command. Keep a fast dev build and a minified build for production:
{
"scripts": {
"dev": "vite",
"build": "vite build"
}
}
For the standalone CLI, the scripts wrap the compiler directly β note the --minify on the production build:
{
"scripts": {
"dev": "@tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch",
"build": "@tailwindcss/cli -i ./src/input.css -o ./dist/output.css --minify"
}
}
Editor tooling that pays for itself
The official Tailwind CSS IntelliSense extension for VS Code is close to essential. It gives you autocomplete for every class, hovers that show the underlying CSS, colour swatches inline, and warnings for typos or conflicting classes. It is like having a Tailwind reference open in your editor at all times.
- Tailwind CSS IntelliSense β autocomplete, hover previews, and linting
- Prettier +
prettier-plugin-tailwindcssβ automatically sorts your utility classes into Tailwind's recommended order on save, so class lists stay consistent across the team
in VS Code] -->|save| B[IntelliSense +
class sorting] B --> C[Watcher rebuilds CSS] C --> D[Browser hot-reloads] D -->|see result| A
This tight loop β edit, save, auto-sort, rebuild, reload β is what makes Tailwind feel fast. Once it's in place you rarely think about the build at all; you just style.
Hands-on: Scaffold a Project
ποΈ Build a live-reloading Tailwind + Vite project
Objective: Go from an empty folder to a page whose styles rebuild the instant you edit the markup.
Instructions:
- Create and enter a project folder, then start a bare Vite app:
npm create vite@latest tailwind-lab -- --template vanilla cd tailwind-lab npm install - Add Tailwind and its Vite plugin:
npm install tailwindcss @tailwindcss/vite - Register the plugin in
vite.config.js(create the file if it doesn't exist), then replace the contents ofstyle.csswith a single line:@import "tailwindcss"; - In
index.html, add a heading using utilities, for example:<h1 class="text-4xl font-bold text-indigo-600 underline"> It works! </h1> - Run
npm run dev, open the local URL, and changetext-indigo-600totext-rose-600. The colour should update without a manual refresh.
π‘ Hint
Make sure style.css is actually imported by your JavaScript entry point (Vite's vanilla template imports it from main.js with import './style.css'). If nothing is styled, that missing import is the usual culprit.
β Solution β vite.config.js
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [tailwindcss()],
});
With this in place, npm run dev gives you hot module replacement: editing any class in index.html re-generates the CSS and updates the page in a fraction of a second. When you're happy, npm run build writes an optimized, minified bundle to dist/.
Best Practices
A few habits keep a Tailwind codebase pleasant to work in as it grows.
| β Do | β Avoid |
|---|---|
| Use the Vite plugin or CLI for anything real | Shipping the play CDN to production |
| Write complete, literal class names | Concatenating class names like bg-${x}-500 |
| Let the class-sorting Prettier plugin order classes | Hand-ordering classes and arguing about it in review |
Reach for the built-in scale (p-4, text-lg) |
Reflexively using arbitrary values everywhere |
| Keep safelists narrow and specific | Broad regex safelists that bloat the bundle |
π‘ Grouping long class lists
When a class list grows unwieldy, that's a signal to extract a component (a React/Vue component, or a reusable @apply-based class you'll meet in the next lesson) rather than to keep piling utilities onto one element. Readable markup beats clever markup.
Summary & Quiz
π Key Takeaways
- Utility-first means composing designs from small classes in your markup instead of writing bespoke CSS.
- Install with the Vite plugin for apps, the CLI for static sites, and the play CDN only for throwaway prototypes.
- The engine scans source as plain text and generates only the classes it finds β which is why builds stay tiny.
- Because scanning is textual, dynamic class names must be complete strings; map them or safelist them.
- A watch-mode script plus IntelliSense and class sorting make the edit-save-reload loop fast.
π― Quick Quiz
Question 1: What best describes Tailwind's "utility-first" approach?
Question 2: Why does className={`bg-${color}-500`} often render with no background?
Question 3: Which install method should you not ship to production?
π Further Reading
- Tailwind Docs β Install with Vite
- Tailwind Docs β Detecting classes in source files
- Tailwind CSS IntelliSense for VS Code
π What's Next?
You can install Tailwind and keep your build lean. Next we'll make it yours β defining a custom colour palette, spacing, and fonts, then packaging repeated patterns into reusable components in Customizing and Extending Tailwind.
π Nice work!
Your Tailwind toolchain is set up and rebuilding on every keystroke. Time to bend it to your design.