Skip to main content

tailwind-theme-builder

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.

Source facts

Repository
jezweb/claude-skills
Last source activity
May 13, 2026 at 10:26
Detected SKILL.md language
English
Stars
1,034
Forks
106

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
8 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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=""`.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub