Environment setup: setting up a React + Tailwind CSS development environment
What you learn in this chapter
You set up the development environment needed for this book, using the latest stack as of 2026. Every tool you install here becomes the foundation for building "reusable components aligned with a design system."
- Vite: a fast build tool and dev server
- React 19: the UI library
- TypeScript: type-safe JavaScript
- Tailwind CSS v4: a utility-first CSS framework
- class-variance-authority (cva): declaratively manage variants (kinds)
- clsx + tailwind-merge: combine class names safely
- radix-ui: accessible UI primitives (such as Slot)
We dig into what each of these does, one at a time, in later chapters. In this chapter, we focus on "getting a working environment."
The development environment you will end up with
The main versions used in this book are as follows (the latest as of June 2026).
| Tool / library | Version | Role |
|---|---|---|
| React | 19.x | UI library |
| Vite | 8.x | Build / dev server |
| TypeScript | 6.x | Type system |
| Tailwind CSS | 4.x | Styling |
| class-variance-authority | 0.7.x | Variant management |
| clsx | 2.x | Conditional class combination |
| tailwind-merge | 3.x | Resolving Tailwind class conflicts |
| radix-ui | 1.x | UI primitives (Slot, etc.) |
Versions go up over time. The code in this book assumes these major versions. You can check the versions actually installed in package.json.
Prerequisites
You need Node.js. To satisfy the requirements of Vite 8, prepare Node.js 22.12 or later (24 LTS recommended).
# Check the Node.js version (22.12 or later, 24 LTS recommended)
node -v
# Check the npm version
npm -v
If Node.js is not installed or your version is old, install the LTS version from the official site.
Step 1: Create the project
Create a React + TypeScript project using Vite.
# Create the project (react-ts template)
npm create vite@latest button-design-guide -- --template react-ts
# Move into the project directory
cd button-design-guide
# Install dependencies
npm install
The directory structure right after creation looks roughly like this.
button-design-guide/
├─ src/
│ ├─ App.tsx
│ ├─ main.tsx
│ └─ index.css
├─ package.json
├─ tsconfig.json # the coordinator of the references
├─ tsconfig.app.json # type settings for the app
├─ tsconfig.node.json # type settings for vite.config
└─ vite.config.ts
In recent Vite templates, the TypeScript configuration is split into three files: tsconfig.json / tsconfig.app.json / tsconfig.node.json. This split becomes relevant later when you configure path aliases.
Step 2: Set up Tailwind CSS v4
In Tailwind CSS v4, the standard approach is to use the dedicated Vite plugin. You can keep everything inside CSS without writing a config file (tailwind.config.js).
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 src/index.css with this single line.
@import "tailwindcss";
That is all it takes to start using Tailwind.
Why is no config file needed in v4?
Up through v3, you had to write things like which files to target (content) in tailwind.config.js. In v4, the design changed to be "CSS-first": it scans automatically starting from @import "tailwindcss" in your CSS, and you define your theme inside CSS (@theme). This makes it easier to manage design tokens centrally as CSS variables, which pairs well with a design system (we cover this in detail in the chapter on design tokens).
If you want to use Tailwind v3, see the appendix at the end of this chapter.
Step 3: Install the required libraries
Install the libraries needed to implement the Button component.
npm install class-variance-authority clsx tailwind-merge radix-ui
The role of each is as follows.
| Library | Role | Chapter to learn more |
|---|---|---|
class-variance-authority | Declaratively manage variants (variant, size) | The cva chapter |
clsx | Combine class names conditionally | The cn chapter |
tailwind-merge | Resolve Tailwind class conflicts | The cn chapter |
radix-ui | UI primitives such as Slot that enable asChild | The Slot chapter |
radix-ui used to be split into per-feature packages such as @radix-ui/react-slot. In 2026 these were consolidated into the unified radix-ui package, and the latest shadcn/ui uses this one too. This book follows the unified version.
Step 4: Create the cn function
Create an important utility function called cn that we use throughout this book.
mkdir -p src/lib
Create src/lib/utils.ts.
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
The cn function is "a function that combines multiple class names safely." We explain in depth what clsx and tailwind-merge do inside it, and why we combine these two, in the cn chapter. For now, it is enough to understand that "we prepared a handy function we will definitely use later."
Step 5: Configure path aliases
Set things up so you can reference the src/ directory with @/, like @/lib/utils. This keeps imports readable even when files are deep in the hierarchy.
① vite.config.ts
Tell Vite about the alias.
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import path from "path"
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
② tsconfig.json
Tell TypeScript about the alias too. Among the split config files, add compilerOptions to tsconfig.json, which is the coordinator.
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
As of TypeScript 6.0, baseUrl became a deprecated hard error (TS5101). So write the paths values as relative paths starting with ./, and do not set baseUrl.
③ tsconfig.app.json
Add the same setting to tsconfig.app.json, which is for the app (both are needed for editor completion and type checking to work).
{
"compilerOptions": {
// ...keep the existing settings as they are...
"paths": {
"@/*": ["./src/*"]
}
}
}
④ Install @types/node
Install the type definitions for path, which we used in vite.config.ts.
npm install -D @types/node
Step 6: Verify it works
Start the dev server and check that the environment runs correctly.
npm run dev
Rewrite src/App.tsx as follows and check that Tailwind is working.
function App() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100">
<div className="rounded-lg bg-white p-8 shadow-lg">
<h1 className="text-2xl font-bold text-gray-800">Setup complete!</h1>
<p className="mt-2 text-gray-600">Tailwind CSS is working correctly.</p>
<button className="mt-4 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Test button
</button>
</div>
</div>
)
}
export default App
Open the URL shown in the terminal (often http://localhost:5173) in your browser. If the styles are applied, it worked.
The port number can change depending on your environment and settings. If 5173 is not used, check the URL shown in the terminal output.
(Shortcut) Using the shadcn/ui CLI
So far, we set up the environment by hand in order to "understand the inner workings." In real work, the shadcn/ui CLI can automate all of this at once.
# Initialize the project for shadcn/ui
npx shadcn@latest init
# Add the Button component
npx shadcn@latest add button
Running init creates a config file, components.json.
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
style: "new-york": the visual style family of the componentstailwind.config: "": empty because Tailwind v4 needs no config filecssVariables: true: a setting to manage colors and the like with CSS variables (design tokens) (covered in the chapter on design tokens)
init writes design tokens into src/index.css and also auto-generates the cn function in src/lib/utils.ts. In other words, the CLI does for you what we did in Steps 2–4.
In this book we assemble things by hand to understand "why it works that way," but the CLI is convenient when you want to spin up a new project quickly. If you use the CLI after understanding the mechanism, you will fully understand what the generated code means.
Appendix: Using Tailwind CSS v3
These are the steps for using v3, for example in an existing project. If you can use v4, feel free to skip this appendix.
npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
Edit the generated tailwind.config.js.
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { extend: {} },
plugins: [],
}
Write src/index.css as follows.
@tailwind base;
@tailwind components;
@tailwind utilities;
In this case, you do not need to add the @tailwindcss/vite plugin to vite.config.ts. The cn function and component code in this book work the same way on both v3 and v4. However, tailwind-merge must match the Tailwind version (for v3, npm install tailwind-merge@2; see the compatibility table in the cn chapter for details).
Project structure
After setup is complete, the structure looks roughly like this.
button-design-guide/
├─ src/
│ ├─ lib/
│ │ └─ utils.ts # the cn function
│ ├─ components/
│ │ └─ ui/ # UI components (to be created)
│ │ └─ button.tsx
│ ├─ App.tsx
│ ├─ main.tsx
│ └─ index.css # @import "tailwindcss"
├─ components.json # only when using the CLI
├─ package.json
├─ tsconfig.json
├─ tsconfig.app.json
└─ vite.config.ts
Summary of this chapter
- Created a project with Vite + React 19 + TypeScript
- Set up Tailwind CSS v4 with the Vite plugin +
@import "tailwindcss"(no config file needed) - Installed
cva/clsx/tailwind-merge/radix-ui - Created the
cnfunction (details in the cn chapter) - Configured the path alias
@/in bothtsconfig.jsonandtsconfig.app.json - In real work, you can auto-build the same environment with
npx shadcn@latest init
Troubleshooting
Tailwind classes don't work
Check that the CSS is loaded in src/main.tsx.
// src/main.tsx
import "./index.css" // make sure this line exists
The path alias (@/) is not recognized
Check all three of the following.
resolve.aliasinvite.config.tspathsintsconfig.jsonandtsconfig.app.json(both are needed; in TS 6, do not writebaseUrl)- Whether
@types/nodeis installed
Module-not-found errors
rm -rf node_modules package-lock.json
npm install
Want to see the details of type errors
npx tsc --noEmit
If it is not resolved, refer to the official documentation.
In the next chapter, we first look at the "finished form" of the Button component we will build, to grasp the big picture.