$npx -y skills add jezweb/claude-skills --skill tailwind-theme-builderSet 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 w
| 1 | # Tailwind Theme Builder |
| 2 | |
| 3 | Set up a fully themed Tailwind v4 + shadcn/ui project with dark mode. Produces configured CSS, theme provider, and working component library. |
| 4 | |
| 5 | ## Architecture: The Four-Step Pattern |
| 6 | |
| 7 | Tailwind v4 requires a specific architecture for CSS variable-based theming. This pattern is **mandatory** -- skipping or modifying steps breaks the theme. |
| 8 | |
| 9 | ### How It Works |
| 10 | |
| 11 | ``` |
| 12 | CSS Variable Definition --> @theme inline Mapping --> Tailwind Utility Class |
| 13 | --background --> --color-background --> bg-background |
| 14 | (with hsl() wrapper) (references variable) (generated class) |
| 15 | ``` |
| 16 | |
| 17 | Dark mode switching: |
| 18 | ``` |
| 19 | ThemeProvider toggles .dark class on <html> |
| 20 | --> CSS variables update automatically (.dark overrides :root) |
| 21 | --> Tailwind utilities reference updated variables |
| 22 | --> UI updates without re-render |
| 23 | ``` |
| 24 | |
| 25 | ### Best Practices |
| 26 | |
| 27 | - **Semantic names:** Use `--primary` not `--blue-500` |
| 28 | - **Foreground pairing:** Every background colour needs a foreground (`--primary` + `--primary-foreground`) |
| 29 | - **WCAG contrast:** Normal text 4.5:1, large text 3:1, UI components 3:1 |
| 30 | - **Chart colours:** Use separate variables with `@theme inline` mapping, reference via `var(--chart-1)` in style props |
| 31 | |
| 32 | --- |
| 33 | |
| 34 | ## Workflow |
| 35 | |
| 36 | ### Step 1: Install Dependencies |
| 37 | |
| 38 | ```bash |
| 39 | pnpm add tailwindcss @tailwindcss/vite |
| 40 | pnpm add -D @types/node tw-animate-css |
| 41 | pnpm dlx shadcn@latest init |
| 42 | |
| 43 | # Delete v3 config if it exists |
| 44 | rm -f tailwind.config.ts |
| 45 | ``` |
| 46 | |
| 47 | ### Step 2: Configure Vite |
| 48 | |
| 49 | Copy `assets/vite.config.ts` or add the Tailwind plugin: |
| 50 | |
| 51 | ```typescript |
| 52 | import { defineConfig } from 'vite' |
| 53 | import react from '@vitejs/plugin-react' |
| 54 | import tailwindcss from '@tailwindcss/vite' |
| 55 | import path from 'path' |
| 56 | |
| 57 | export default defineConfig({ |
| 58 | plugins: [react(), tailwindcss()], |
| 59 | resolve: { alias: { '@': path.resolve(__dirname, './src') } } |
| 60 | }) |
| 61 | ``` |
| 62 | |
| 63 | ### Step 3: Four-Step CSS Architecture (Mandatory) |
| 64 | |
| 65 | This exact order is required. Skipping steps breaks the theme. |
| 66 | |
| 67 | **src/index.css:** |
| 68 | |
| 69 | ```css |
| 70 | @import "tailwindcss"; |
| 71 | @import "tw-animate-css"; |
| 72 | |
| 73 | /* 1. Define CSS variables at root (NOT inside @layer base) */ |
| 74 | :root { |
| 75 | --background: hsl(0 0% 100%); |
| 76 | --foreground: hsl(222.2 84% 4.9%); |
| 77 | --primary: hsl(221.2 83.2% 53.3%); |
| 78 | --primary-foreground: hsl(210 40% 98%); |
| 79 | /* ... all semantic tokens */ |
| 80 | } |
| 81 | |
| 82 | .dark { |
| 83 | --background: hsl(222.2 84% 4.9%); |
| 84 | --foreground: hsl(210 40% 98%); |
| 85 | --primary: hsl(217.2 91.2% 59.8%); |
| 86 | --primary-foreground: hsl(222.2 47.4% 11.2%); |
| 87 | } |
| 88 | |
| 89 | /* 2. Map variables to Tailwind utilities */ |
| 90 | @theme inline { |
| 91 | --color-background: var(--background); |
| 92 | --color-foreground: var(--foreground); |
| 93 | --color-primary: var(--primary); |
| 94 | --color-primary-foreground: var(--primary-foreground); |
| 95 | } |
| 96 | |
| 97 | /* 3. Apply base styles (NO hsl() wrapper here) */ |
| 98 | @layer base { |
| 99 | body { |
| 100 | background-color: var(--background); |
| 101 | color: var(--foreground); |
| 102 | } |
| 103 | } |
| 104 | ``` |
| 105 | |
| 106 | **Result:** `bg-background`, `text-primary` etc. work automatically. Dark mode switches via `.dark` class -- no `dark:` variants needed for semantic colours. |
| 107 | |
| 108 | ### Step 4: Set Up Dark Mode |
| 109 | |
| 110 | Copy `assets/theme-provider.tsx` to your components directory, then wrap your app: |
| 111 | |
| 112 | ```typescript |
| 113 | import { ThemeProvider } from '@/components/theme-provider' |
| 114 | |
| 115 | ReactDOM.createRoot(document.getElementById('root')!).render( |
| 116 | <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> |
| 117 | <App /> |
| 118 | </ThemeProvider> |
| 119 | ) |
| 120 | ``` |
| 121 | |
| 122 | Add a theme toggle -- install the dropdown menu then use the ModeToggle component below: |
| 123 | |
| 124 | ```bash |
| 125 | pnpm dlx shadcn@latest add dropdown-menu |
| 126 | ``` |
| 127 | |
| 128 | ```typescript |
| 129 | // src/components/mode-toggle.tsx |
| 130 | import { Moon, Sun } from "lucide-react" |
| 131 | import { Button } from "@/components/ui/button" |
| 132 | import { |
| 133 | DropdownMenu, |
| 134 | DropdownMenuContent, |
| 135 | DropdownMenuItem, |
| 136 | DropdownMenuTrigger, |
| 137 | } from "@/components/ui/dropdown-menu" |
| 138 | import { useTheme } from "@/components/theme-provider" |
| 139 | |
| 140 | export function ModeToggle() { |
| 141 | const { setTheme } = useTheme() |
| 142 | |
| 143 | return ( |
| 144 | <DropdownMenu> |
| 145 | <DropdownMenuTrigger asChild> |
| 146 | <Button variant="outline" size="icon"> |
| 147 | <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> |
| 148 | <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> |
| 149 | <span className="sr-only">Toggle theme</span> |
| 150 | </Button> |
| 151 | </DropdownMenuTrigger> |
| 152 | <DropdownMenuContent align="end"> |
| 153 | <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem> |
| 154 | <DropdownMenuItem onClick={() => setTheme("dark")} |