4.2 KiB
4.2 KiB
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
: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
.darkfor dark mode overrides (NOT.dark { @theme { } }) - ❌ Never put
:rootor.darkinside@layer base
Step 2: Map Variables to Tailwind Utilities
@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.tsfor colors @theme inlinegenerates utility classes (bg-background,text-foreground)- Without this, utilities like
bg-primarywon't exist
Step 3: Apply Base Styles
@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:
<div className="bg-background text-foreground">works automatically- No
dark:variants needed in components - Theme switches via
.darkclass on<html> - 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 <html>
↓
CSS variables update automatically (.dark overrides)
↓
Tailwind utilities reference updated variables
↓
UI updates without re-render
Common Mistakes
❌ Mistake 1: Variables Inside @layer base
/* 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 { } }
/* 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
/* 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
// 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:
--primary /* ✅ Semantic */
--blue-500 /* ❌ Not semantic */
2. Foreground Pairing
Every background color needs a foreground:
--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):
:root {
--chart-1: hsl(12 76% 61%);
}
@theme inline {
--color-chart-1: var(--chart-1);
}
Use in components:
<div style={{ backgroundColor: 'var(--chart-1)' }} />
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