Skill 寫法圖鑑

tailwind-theme-builder

jezweb | 設計與前端 | 原文 | MIT

設定 Tailwind v4 加 shadcn/ui 主題與 dark mode,附 18 個陷阱

中文翻譯

---
name: tailwind-theme-builder
description: "設定 Tailwind v4 + shadcn/ui 的主題化 UI,含 dark mode。安裝相依套件、透過 @theme inline 設定 CSS 變數、接上 dark mode 切換、驗證。只要使用者提到 Tailwind v4、設定 Tailwind 主題、shadcn/ui 顏色、dark mode,或排查顏色沒作用、tw-animate-css 錯誤、@theme inline 衝突、升級後 @apply 壞掉、v3 → v4 遷移問題,就用這個 skill。"
compatibility: claude-code-only
---

# Tailwind Theme Builder

設定一個完整主題化的 Tailwind v4 + shadcn/ui 專案,含 dark mode。產出設定好的 CSS、theme provider,以及可用的元件庫。

## 架構:四步驟模式

Tailwind v4 要做 CSS 變數式的主題,需要特定的架構。這個模式是**強制的**,跳過或改動任何步驟都會讓主題壞掉。

### 運作方式

```
CSS Variable Definition --> @theme inline Mapping --> Tailwind Utility Class
--background           --> --color-background     --> bg-background
(with hsl() wrapper)      (references variable)     (generated class)
```

Dark mode 切換:
```
ThemeProvider toggles .dark class on <html>
  --> CSS variables update automatically (.dark overrides :root)
  --> Tailwind utilities reference updated variables
  --> UI updates without re-render
```

### 最佳實踐

- **語意化命名:** 用 `--primary`,不要用 `--blue-500`
- **前景色配對:** 每個背景色都要有對應的前景色(`--primary` + `--primary-foreground`)
- **WCAG 對比:** 一般文字 4.5:1,大字 3:1,UI 元件 3:1
- **圖表顏色:** 用獨立的變數搭配 `@theme inline` 對應,在 style props 裡用 `var(--chart-1)` 引用

---

## 工作流程

### 步驟 1:安裝相依套件

```bash
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node tw-animate-css
pnpm dlx shadcn@latest init

# Delete v3 config if it exists
rm -f tailwind.config.ts
```

### 步驟 2:設定 Vite

複製 `assets/vite.config.ts`,或自己加上 Tailwind plugin:

```typescript
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') } }
})
```

### 步驟 3:四步驟 CSS 架構(強制)

一定要照這個順序。跳過步驟會讓主題壞掉。

**src/index.css:**

```css
@import "tailwindcss";
@import "tw-animate-css";

/* 1. Define CSS variables at root (NOT inside @layer base) */
:root {
  --background: hsl(0 0% 100%);
  --foreground: hsl(222.2 84% 4.9%);
  --primary: hsl(221.2 83.2% 53.3%);
  --primary-foreground: hsl(210 40% 98%);
  /* ... all semantic tokens */
}

.dark {
  --background: hsl(222.2 84% 4.9%);
  --foreground: hsl(210 40% 98%);
  --primary: hsl(217.2 91.2% 59.8%);
  --primary-foreground: hsl(222.2 47.4% 11.2%);
}

/* 2. Map variables to Tailwind utilities */
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
}

/* 3. Apply base styles (NO hsl() wrapper here) */
@layer base {
  body {
    background-color: var(--background);
    color: var(--foreground);
  }
}
```

**結果:** `bg-background`、`text-primary` 等 class 會自動生效。Dark mode 透過 `.dark` class 切換,語意化顏色不需要 `dark:` variant。

### 步驟 4:設定 Dark Mode

把 `assets/theme-provider.tsx` 複製到你的 components 目錄,然後包住你的 app:

```typescript
import { ThemeProvider } from '@/components/theme-provider'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
    <App />
  </ThemeProvider>
)
```

加上主題切換:先安裝 dropdown menu,再使用下面的 ModeToggle 元件:

```bash
pnpm dlx shadcn@latest add dropdown-menu
```

```typescript
// src/components/mode-toggle.tsx
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useTheme } from "@/components/theme-provider"

export function ModeToggle() {
  const { setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### 步驟 5:設定 components.json

```json
{
  "tailwind": {
    "config": "",
    "css": "src/index.css",
    "baseColor": "slate",
    "cssVariables": true
  }
}
```

`"config": ""` 非常關鍵,v4 不使用 tailwind.config.ts。

---

## 關鍵規則

**一定要:**
- 在 `:root`/`.dark` 裡用 `hsl()` 包住顏色
- 用 `@theme inline` 對應所有 CSS 變數
- 用 `@tailwindcss/vite` plugin(不要用 PostCSS)
- 如果 `tailwind.config.ts` 存在,就把它刪掉

**絕對不要:**
- 把 `:root`/`.dark` 放進 `@layer base`
- 用 `.dark { @theme { } }`(v4 不支援巢狀 @theme)
- 重複包裝:`hsl(var(--background))`
- 對 `@layer base` 裡的 class 使用 `@apply`(改用 `@utility`)

---

## 全部 18 個陷阱

### 快速診斷

| # | 症狀 | 原因 | 解法 |
|---|---------|-------|-----|
| 1 | 變數被忽略 / 主題壞掉 | `:root` 在 `@layer base` 裡 | 把 `:root` 和 `.dark` 移到最外層 |
| 2 | Dark mode 顏色沒切換 | `.dark { @theme { } }` | 用 CSS 變數 + 單一個 `@theme inline` |
| 3 | 顏色全是黑/白 | `hsl()` 重複包裝 | 用 `var(--background)`,不要用 `hsl(var(...))` |
| 4 | `bg-primary` 沒被產生 | 顏色放在 `tailwind.config.ts` | 刪掉 config,改用 `@theme inline` |
| 5 | 缺少 `bg-background` class | 沒有 `@theme inline` 區塊 | 加上 `@theme inline` 對應變數 |
| 6 | shadcn 元件壞掉 | `components.json` 有 config 路徑 | 設成 `"config": ""`(空字串) |
| 7 | Tailwind 沒在處理 | 用了 PostCSS plugin | 換成 `@tailwindcss/vite` plugin |
| 8 | `@/` import 失敗 | 缺少 path alias | 在 `tsconfig.app.json` 加上 `paths` |
| 9 | 多餘的 `dark:` variant | 用了 `dark:bg-primary-dark` | 直接用 `bg-primary`,變數會處理 |
| 10 | 到處都是寫死的顏色 | 用了 `bg-blue-600 dark:bg-blue-400` | 用語意 token:`bg-primary` |
| 11 | Class 合併的 bug | 用字串拼接 class | 用 `@/lib/utils` 的 `cn()` |
| 12 | Radix Select 當掉 | 空字串值 `value=""` | 用 `value="placeholder"` |
| 13 | Tailwind 版本錯誤 | 裝了 `tailwindcss@^3` | 裝 `tailwindcss@^4.1.0` + `@tailwindcss/vite` |
| 14 | 缺 peer dependency | 只裝了 `tailwindcss` | 也要裝 `clsx`、`tailwind-merge`、`@types/node` |
| 15 | Dark mode 下壞掉 | 只測過 light mode | 測 light、dark、system,以及切換時的轉場 |
| 16 | 沒過 WCAG 對比 | 肉眼看起來沒問題 | 檢查比例:一般文字 4.5:1,大字/UI 3:1 |
| 17 | 動畫 import 讓 build 失敗 | 用了 `tailwindcss-animate`(已棄用) | 用 `tw-animate-css` 或原生 CSS 動畫 |
| 18 | CSS 優先順序問題 | shadcn init 之後出現重複的 `@layer base` | 合併成單一個 `@layer base` 區塊 |

### 陷阱細節與程式碼範例

**#1 -- :root 在 @layer base 裡**

Tailwind v4 會把 `@theme`/`@layer` 以外的 CSS 去掉,但 `:root` 必須在最外層才會保留。這是最常見的設定失敗原因。

錯誤:
```css
@layer base {
  :root { --background: hsl(0 0% 100%); }
}
```

正確:
```css
:root { --background: hsl(0 0% 100%); }
@layer base {
  body { background-color: var(--background); }
}
```

**#2 -- 巢狀 @theme**

Tailwind v4 不支援把 `@theme` 放在 selector 裡面。用 `:root`/`.dark` 裡的 CSS 變數,加上單一個 `@theme inline` 區塊。

錯誤:
```css
@theme { --color-primary: hsl(0 0% 0%); }
.dark { @theme { --color-primary: hsl(0 0% 100%); } }
```

正確:
```css
:root { --primary: hsl(0 0% 0%); }
.dark { --primary: hsl(0 0% 100%); }
@theme inline { --color-primary: var(--primary); }
```

**#3 -- hsl() 重複包裝**

變數本身已經含有 `hsl()`。重複包裝會變成 `hsl(hsl(...))`。

錯誤:`background-color: hsl(var(--background));`
正確:`background-color: var(--background);`

**#4 -- 顏色放在 tailwind.config.ts**

Tailwind v4 完全忽略 config 檔裡的 `theme.extend.colors`。把檔案刪掉,或留空。在 `components.json` 設 `"config": ""`。

**#5 -- 缺少 @theme inline**

沒有 `@theme inline` 的話,Tailwind 根本不知道你的 CSS 變數。像 `bg-background` 這類 utility class 就是不會被產生。

錯誤:
```css
:root { --background: hsl(0 0% 100%); }
/* No @theme inline block -- bg-background won't exist */
```

正確:
```css
:root { --background: hsl(0 0% 100%); }
@theme inline { --color-background: var(--background); }
```

**#7 -- PostCSS vs Vite plugin**

錯誤:
```typescript
export default defineConfig({
  css: { postcss: './postcss.config.js' }  // Old v3 way
})
```

正確:
```typescript
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
  plugins: [react(), tailwindcss()]  // v4 way
})
```

**#8 -- Path alias**

加到 `tsconfig.app.json`:
```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] }
  }
}
```

**#11 -- 用 cn() 工具函式合併 class**

錯誤:`` className={`base ${isActive && 'active'}`} ``
正確:`className={cn("base", isActive && "active")}`

`@/lib/utils` 的 `cn()` 會正確合併並去除重複的 Tailwind class。

**#12 -- Radix Select 的空值**

Radix UI Select 不允許空字串值。用 `value="placeholder"` 取代 `value=""`。

**#14 -- 必要的相依套件**

```json
{
  "dependencies": {
    "tailwindcss": "^4.1.0",
    "@tailwindcss/vite": "^4.1.0",
    "clsx": "^2.1.1",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@types/node": "^24.0.0"
  }
}
```

**#17 -- tw-animate-css**

`tailwindcss-animate` 在 Tailwind v4 已經棄用。shadcn/ui 文件可能還在引用它。會造成 build 失敗和 import 錯誤。改用 `tw-animate-css` 或 `@tailwindcss/motion`。

**#18 -- shadcn init 之後重複的 @layer base**

`shadcn init` 會加上它自己的 `@layer base` 區塊。跑完 init 之後馬上檢查 `src/index.css`,把重複的區塊合併成一個。

錯誤:
```css
@layer base { body { background-color: var(--background); } }
@layer base { * { border-color: hsl(var(--border)); } }  /* duplicate from shadcn */
```

正確:
```css
@layer base {
  * { border-color: var(--border); }
  body { background-color: var(--background); color: var(--foreground); }
}
```

### 預防檢查清單

- [ ] 沒有 `tailwind.config.ts` 檔案(或它是空的)
- [ ] `components.json` 有 `"config": ""`
- [ ] `:root` 裡所有顏色都有 `hsl()` 包裝
- [ ] `@theme inline` 對應了所有變數
- [ ] `@layer base` 沒有包住 `:root`
- [ ] Theme provider 包住整個 app
- [ ] 在 light、dark、system 模式下都測過
- [ ] 所有文字都有足夠的對比

---

## Dark Mode 測試檢查清單

- [ ] Light mode 顯示正確
- [ ] Dark mode 顯示正確
- [ ] System mode 會遵循作業系統設定
- [ ] 重新整理頁面後主題仍然保留
- [ ] 切換元件顯示目前狀態
- [ ] 所有文字都有適當的對比
- [ ] 載入時沒有錯誤主題閃一下
- [ ] 在無痕模式下也能運作(優雅降級)

---

## 資產檔案

從 `assets/` 目錄複製:
- `index.css`:完整的 CSS,含所有顏色變數
- `components.json`:shadcn/ui v4 設定
- `vite.config.ts`:Vite + Tailwind plugin
- `theme-provider.tsx`:Dark mode provider
- `utils.ts`:`cn()` 工具函式

## 參考檔案

- `references/migration-guide.md`:v3 到 v4 遷移

## 官方文件

- shadcn/ui Tailwind v4 指南:https://ui.shadcn.com/docs/tailwind-v4
- shadcn/ui Dark Mode(Vite):https://ui.shadcn.com/docs/dark-mode/vite
- shadcn/ui Theming:https://ui.shadcn.com/docs/theming
- Tailwind v4 文件:https://tailwindcss.com/docs
- Tailwind Dark Mode:https://tailwindcss.com/docs/dark-mode

英文原文

---
name: tailwind-theme-builder
description: "Set up Tailwind v4 + shadcn/ui themed UI with dark mode. Install deps, configure CSS variables via @theme inline, wire dark mode toggle, verify. Use whenever the user mentions Tailwind v4, setting up Tailwind theming, shadcn/ui colours, dark mode, or troubleshooting colours not working, tw-animate-css errors, @theme inline conflicts, @apply breaking after upgrade, or v3 → v4 migration issues."
compatibility: claude-code-only
---

# Tailwind Theme Builder

Set up a fully themed Tailwind v4 + shadcn/ui project with dark mode. Produces configured CSS, theme provider, and working component library.

## Architecture: The Four-Step Pattern

Tailwind v4 requires a specific architecture for CSS variable-based theming. This pattern is **mandatory** -- skipping or modifying steps breaks the theme.

### How It Works

```
CSS Variable Definition --> @theme inline Mapping --> Tailwind Utility Class
--background           --> --color-background     --> bg-background
(with hsl() wrapper)      (references variable)     (generated class)
```

Dark mode switching:
```
ThemeProvider toggles .dark class on <html>
  --> CSS variables update automatically (.dark overrides :root)
  --> Tailwind utilities reference updated variables
  --> UI updates without re-render
```

### Best Practices

- **Semantic names:** Use `--primary` not `--blue-500`
- **Foreground pairing:** Every background colour needs a foreground (`--primary` + `--primary-foreground`)
- **WCAG contrast:** Normal text 4.5:1, large text 3:1, UI components 3:1
- **Chart colours:** Use separate variables with `@theme inline` mapping, reference via `var(--chart-1)` in style props

---

## Workflow

### Step 1: Install Dependencies

```bash
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node tw-animate-css
pnpm dlx shadcn@latest init

# Delete v3 config if it exists
rm -f tailwind.config.ts
```

### Step 2: Configure Vite

Copy `assets/vite.config.ts` or add the Tailwind plugin:

```typescript
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') } }
})
```

### Step 3: Four-Step CSS Architecture (Mandatory)

This exact order is required. Skipping steps breaks the theme.

**src/index.css:**

```css
@import "tailwindcss";
@import "tw-animate-css";

/* 1. Define CSS variables at root (NOT inside @layer base) */
:root {
  --background: hsl(0 0% 100%);
  --foreground: hsl(222.2 84% 4.9%);
  --primary: hsl(221.2 83.2% 53.3%);
  --primary-foreground: hsl(210 40% 98%);
  /* ... all semantic tokens */
}

.dark {
  --background: hsl(222.2 84% 4.9%);
  --foreground: hsl(210 40% 98%);
  --primary: hsl(217.2 91.2% 59.8%);
  --primary-foreground: hsl(222.2 47.4% 11.2%);
}

/* 2. Map variables to Tailwind utilities */
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
}

/* 3. Apply base styles (NO hsl() wrapper here) */
@layer base {
  body {
    background-color: var(--background);
    color: var(--foreground);
  }
}
```

**Result:** `bg-background`, `text-primary` etc. work automatically. Dark mode switches via `.dark` class -- no `dark:` variants needed for semantic colours.

### Step 4: Set Up Dark Mode

Copy `assets/theme-provider.tsx` to your components directory, then wrap your app:

```typescript
import { ThemeProvider } from '@/components/theme-provider'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
    <App />
  </ThemeProvider>
)
```

Add a theme toggle -- install the dropdown menu then use the ModeToggle component below:

```bash
pnpm dlx shadcn@latest add dropdown-menu
```

```typescript
// src/components/mode-toggle.tsx
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useTheme } from "@/components/theme-provider"

export function ModeToggle() {
  const { setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Step 5: Configure components.json

```json
{
  "tailwind": {
    "config": "",
    "css": "src/index.css",
    "baseColor": "slate",
    "cssVariables": true
  }
}
```

`"config": ""` is critical -- v4 doesn't use tailwind.config.ts.

---

## Critical Rules

**Always:**
- Wrap colours with `hsl()` in `:root`/`.dark`
- Use `@theme inline` to map all CSS variables
- Use `@tailwindcss/vite` plugin (NOT PostCSS)
- Delete `tailwind.config.ts` if it exists

**Never:**
- Put `:root`/`.dark` inside `@layer base`
- Use `.dark { @theme { } }` (v4 doesn't support nested @theme)
- Double-wrap: `hsl(var(--background))`
- Use `@apply` with `@layer base` classes (use `@utility` instead)

---

## All 18 Gotchas

### Quick Diagnosis

| # | Symptom | Cause | Fix |
|---|---------|-------|-----|
| 1 | Variables ignored / theme broken | `:root` inside `@layer base` | Move `:root` and `.dark` to root level |
| 2 | Dark mode colours not switching | `.dark { @theme { } }` | Use CSS variables + single `@theme inline` |
| 3 | Colours all black/white | Double `hsl()` wrapping | Use `var(--background)` not `hsl(var(...))` |
| 4 | `bg-primary` not generated | Colours in `tailwind.config.ts` | Delete config, use `@theme inline` |
| 5 | `bg-background` class missing | No `@theme inline` block | Add `@theme inline` mapping variables |
| 6 | shadcn components break | `components.json` has config path | Set `"config": ""` (empty string) |
| 7 | Tailwind not processing | Using PostCSS plugin | Switch to `@tailwindcss/vite` plugin |
| 8 | `@/` imports fail | Missing path aliases | Add `paths` to `tsconfig.app.json` |
| 9 | Redundant `dark:` variants | Using `dark:bg-primary-dark` | Just use `bg-primary` -- variables handle it |
| 10 | Hardcoded colours everywhere | Using `bg-blue-600 dark:bg-blue-400` | Use semantic tokens: `bg-primary` |
| 11 | Class merging bugs | String concatenation for classes | Use `cn()` from `@/lib/utils` |
| 12 | Radix Select crashes | Empty string value `value=""` | Use `value="placeholder"` |
| 13 | Wrong Tailwind version | Installed `tailwindcss@^3` | Install `tailwindcss@^4.1.0` + `@tailwindcss/vite` |
| 14 | Missing peer deps | Only installed `tailwindcss` | Also install `clsx`, `tailwind-merge`, `@types/node` |
| 15 | Broken in dark mode | Only tested light mode | Test light, dark, system, and toggle transitions |
| 16 | Fails WCAG contrast | Looks fine visually | Check ratios: 4.5:1 normal text, 3:1 large/UI |
| 17 | Build fails on animation import | Using `tailwindcss-animate` (deprecated) | Use `tw-animate-css` or native CSS animations |
| 18 | CSS priority issues | Duplicate `@layer base` after shadcn init | Merge into single `@layer base` block |

### Gotcha Details with Code Examples

**#1 -- :root inside @layer base**

Tailwind v4 strips CSS outside `@theme`/`@layer`, but `:root` must be at root level to persist. This is the most common setup failure.

WRONG:
```css
@layer base {
  :root { --background: hsl(0 0% 100%); }
}
```

CORRECT:
```css
:root { --background: hsl(0 0% 100%); }
@layer base {
  body { background-color: var(--background); }
}
```

**#2 -- Nested @theme**

Tailwind v4 does not support `@theme` inside selectors. Use CSS variables in `:root`/`.dark` with a single `@theme inline` block.

WRONG:
```css
@theme { --color-primary: hsl(0 0% 0%); }
.dark { @theme { --color-primary: hsl(0 0% 100%); } }
```

CORRECT:
```css
:root { --primary: hsl(0 0% 0%); }
.dark { --primary: hsl(0 0% 100%); }
@theme inline { --color-primary: var(--primary); }
```

**#3 -- Double hsl() wrapping**

Variables already contain `hsl()`. Double-wrapping creates `hsl(hsl(...))`.

WRONG: `background-color: hsl(var(--background));`
CORRECT: `background-color: var(--background);`

**#4 -- Colours in tailwind.config.ts**

Tailwind v4 completely ignores `theme.extend.colors` in config files. Delete the file or leave it empty. Set `"config": ""` in `components.json`.

**#5 -- Missing @theme inline**

Without `@theme inline`, Tailwind has no knowledge of your CSS variables. Utility classes like `bg-background` simply won't be generated.

WRONG:
```css
:root { --background: hsl(0 0% 100%); }
/* No @theme inline block -- bg-background won't exist */
```

CORRECT:
```css
:root { --background: hsl(0 0% 100%); }
@theme inline { --color-background: var(--background); }
```

**#7 -- PostCSS vs Vite plugin**

WRONG:
```typescript
export default defineConfig({
  css: { postcss: './postcss.config.js' }  // Old v3 way
})
```

CORRECT:
```typescript
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
  plugins: [react(), tailwindcss()]  // v4 way
})
```

**#8 -- Path aliases**

Add to `tsconfig.app.json`:
```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] }
  }
}
```

**#11 -- cn() utility for class merging**

WRONG: `` className={`base ${isActive && 'active'}`} ``
CORRECT: `className={cn("base", isActive && "active")}`

`cn()` from `@/lib/utils` properly merges and deduplicates Tailwind classes.

**#12 -- Radix Select empty value**

Radix UI Select does not allow empty string values. Use `value="placeholder"` instead of `value=""`.

**#14 -- Required dependencies**

```json
{
  "dependencies": {
    "tailwindcss": "^4.1.0",
    "@tailwindcss/vite": "^4.1.0",
    "clsx": "^2.1.1",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@types/node": "^24.0.0"
  }
}
```

**#17 -- tw-animate-css**

`tailwindcss-animate` is deprecated in Tailwind v4. shadcn/ui docs may still reference it. Causes build failures and import errors. Use `tw-animate-css` or `@tailwindcss/motion` instead.

**#18 -- Duplicate @layer base after shadcn init**

`shadcn init` adds its own `@layer base` block. Check `src/index.css` immediately after running init and merge any duplicate blocks into one.

WRONG:
```css
@layer base { body { background-color: var(--background); } }
@layer base { * { border-color: hsl(var(--border)); } }  /* duplicate from shadcn */
```

CORRECT:
```css
@layer base {
  * { border-color: var(--border); }
  body { background-color: var(--background); color: var(--foreground); }
}
```

### Prevention Checklist

- [ ] No `tailwind.config.ts` file (or it's empty)
- [ ] `components.json` has `"config": ""`
- [ ] All colors have `hsl()` wrapper in `:root`
- [ ] `@theme inline` maps all variables
- [ ] `@layer base` doesn't wrap `:root`
- [ ] Theme provider wraps app
- [ ] Tested in light, dark, and system modes
- [ ] All text has sufficient contrast

---

## Dark Mode Testing Checklist

- [ ] Light mode displays correctly
- [ ] Dark mode displays correctly
- [ ] System mode respects OS setting
- [ ] Theme persists after page refresh
- [ ] Toggle component shows current state
- [ ] All text has proper contrast
- [ ] No flash of wrong theme on load
- [ ] Works in incognito mode (graceful fallback)

---

## Asset Files

Copy from `assets/` directory:
- `index.css` -- Complete CSS with all colour variables
- `components.json` -- shadcn/ui v4 config
- `vite.config.ts` -- Vite + Tailwind plugin
- `theme-provider.tsx` -- Dark mode provider
- `utils.ts` -- `cn()` utility

## Reference Files

- `references/migration-guide.md` -- v3 to v4 migration

## Official Documentation

- shadcn/ui Tailwind v4 Guide: https://ui.shadcn.com/docs/tailwind-v4
- shadcn/ui Dark Mode (Vite): https://ui.shadcn.com/docs/dark-mode/vite
- shadcn/ui Theming: https://ui.shadcn.com/docs/theming
- Tailwind v4 Docs: https://tailwindcss.com/docs
- Tailwind Dark Mode: https://tailwindcss.com/docs/dark-mode