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
Dark mode switching:
Best Practices
- Semantic names: Use
--primarynot--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 inlinemapping, reference viavar(--chart-1)in style props
Workflow
Step 1: Install Dependencies
Step 2: Configure Vite
Copy assets/vite.config.ts or add the Tailwind plugin:
Step 3: Four-Step CSS Architecture (Mandatory)
This exact order is required. Skipping steps breaks the theme.
src/index.css:
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:
Add a theme toggle -- install the dropdown menu then use the ModeToggle component below:
Step 5: Configure components.json
"config": "" is critical -- v4 doesn't use tailwind.config.ts.
Critical Rules
Always:
- Wrap colours with
hsl()in:root/.dark - Use
@theme inlineto map all CSS variables - Use
@tailwindcss/viteplugin (NOT PostCSS) - Delete
tailwind.config.tsif it exists
Never:
- Put
:root/.darkinside@layer base - Use
.dark { @theme { } }(v4 doesn't support nested @theme) - Double-wrap:
hsl(var(--background)) - Use
@applywith@layer baseclasses (use@utilityinstead)
All 18 Gotchas
Quick Diagnosis
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:
CORRECT:
#2 -- Nested @theme
Tailwind v4 does not support @theme inside selectors. Use CSS variables in :root/.dark with a single @theme inline block.
WRONG:
CORRECT:
#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:
CORRECT:
#7 -- PostCSS vs Vite plugin
WRONG:
CORRECT:
#8 -- Path aliases
Add to tsconfig.app.json:
#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
#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:
CORRECT:
Prevention Checklist
- No
tailwind.config.tsfile (or it's empty) -
components.jsonhas"config": "" - All colors have
hsl()wrapper in:root -
@theme inlinemaps all variables -
@layer basedoesn'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 variablescomponents.json-- shadcn/ui v4 configvite.config.ts-- Vite + Tailwind plugintheme-provider.tsx-- Dark mode providerutils.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


