Skip to main content

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 / libraryVersionRole
React19.xUI library
Vite8.xBuild / dev server
TypeScript6.xType system
Tailwind CSS4.xStyling
class-variance-authority0.7.xVariant management
clsx2.xConditional class combination
tailwind-merge3.xResolving Tailwind class conflicts
radix-ui1.xUI primitives (Slot, etc.)
info

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
warning

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
info

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.

info

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.

LibraryRoleChapter to learn more
class-variance-authorityDeclaratively manage variants (variant, size)The cva chapter
clsxCombine class names conditionallyThe cn chapter
tailwind-mergeResolve Tailwind class conflictsThe cn chapter
radix-uiUI primitives such as Slot that enable asChildThe Slot chapter
info

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))
}
info

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/*"]
}
}
}
info

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.

info

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 components
  • tailwind.config: "": empty because Tailwind v4 needs no config file
  • cssVariables: 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.

info

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 cn function (details in the cn chapter)
  • Configured the path alias @/ in both tsconfig.json and tsconfig.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.

  1. resolve.alias in vite.config.ts
  2. paths in tsconfig.json and tsconfig.app.json (both are needed; in TS 6, do not write baseUrl)
  3. Whether @types/node is 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
info

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.