# Tailwind v4 + shadcn/ui Theming Architecture ## The Four-Step Pattern Tailwind v4 requires a specific architecture for CSS variable-based theming. This pattern is **mandatory** - skipping or modifying steps will break your theme. ### Step 1: Define CSS Variables at Root Level ```css :root { --background: hsl(0 0% 100%); --foreground: hsl(222.2 84% 4.9%); /* ... more colors */ } .dark { --background: hsl(222.2 84% 4.9%); --foreground: hsl(210 40% 98%); /* ... dark mode colors */ } ``` **Critical Rules:** - ✅ Define at root level (NOT inside `@layer base`) - ✅ Use `hsl()` wrapper on all color values - ✅ Use `.dark` for dark mode overrides (NOT `.dark { @theme { } }`) - ❌ Never put `:root` or `.dark` inside `@layer base` ### Step 2: Map Variables to Tailwind Utilities ```css @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); /* ... map all CSS variables */ } ``` **Why This Is Required:** - Tailwind v4 doesn't read `tailwind.config.ts` for colors - `@theme inline` generates utility classes (`bg-background`, `text-foreground`) - Without this, utilities like `bg-primary` won't exist ### Step 3: Apply Base Styles ```css @layer base { body { background-color: var(--background); /* NO hsl() wrapper here */ color: var(--foreground); } } ``` **Critical Rules:** - ✅ Reference variables directly: `var(--background)` - ❌ Never double-wrap: `hsl(var(--background))` (already has hsl) ### Step 4: Result - Automatic Dark Mode With this architecture: - `
` works automatically - No `dark:` variants needed in components - Theme switches via `.dark` class on `` - Single source of truth for all colors --- ## Why This Architecture Works ### Color Variable Flow ``` 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 ↓ CSS variables update automatically (.dark overrides) ↓ Tailwind utilities reference updated variables ↓ UI updates without re-render ``` --- ## Common Mistakes ### ❌ Mistake 1: Variables Inside @layer base ```css /* WRONG */ @layer base { :root { --background: hsl(0 0% 100%); } } ``` **Why It Fails:** Tailwind v4 strips CSS outside `@theme`/`@layer`, but `:root` must be at root level to persist. ### ❌ Mistake 2: Using .dark { @theme { } } ```css /* WRONG */ @theme { --color-primary: hsl(0 0% 0%); } .dark { @theme { --color-primary: hsl(0 0% 100%); } } ``` **Why It Fails:** Tailwind v4 doesn't support nested `@theme` directives. ### ❌ Mistake 3: Double hsl() Wrapping ```css /* WRONG */ @layer base { body { background-color: hsl(var(--background)); } } ``` **Why It Fails:** `--background` already contains `hsl()`, results in `hsl(hsl(...))`. ### ❌ Mistake 4: Config-Based Colors ```typescript // WRONG (tailwind.config.ts) export default { theme: { extend: { colors: { primary: 'hsl(var(--primary))' } } } } ``` **Why It Fails:** Tailwind v4 completely ignores `theme.extend.colors` in config files. --- ## Best Practices ### 1. Semantic Color Names Use semantic names, not color values: ```css --primary /* ✅ Semantic */ --blue-500 /* ❌ Not semantic */ ``` ### 2. Foreground Pairing Every background color needs a foreground: ```css --primary: hsl(...); --primary-foreground: hsl(...); ``` ### 3. WCAG Contrast Ratios Ensure proper contrast: - Normal text: 4.5:1 minimum - Large text: 3:1 minimum - UI components: 3:1 minimum ### 4. Chart Colors Charts need separate variables (don't use hsl wrapper in components): ```css :root { --chart-1: hsl(12 76% 61%); } @theme inline { --color-chart-1: var(--chart-1); } ``` Use in components: ```tsx
``` --- ## Official Documentation - shadcn/ui Tailwind v4 Guide: https://ui.shadcn.com/docs/tailwind-v4 - Tailwind v4 Docs: https://tailwindcss.com/docs - shadcn/ui Theming: https://ui.shadcn.com/docs/theming