8.5 KiB
shadcn/ui Theming & Customization
Theme configuration, CSS variables, dark mode, and component customization.
Dark Mode Setup
Next.js App Router
1. Install next-themes:
npm install next-themes
2. Create theme provider:
// components/theme-provider.tsx
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
3. Wrap app:
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
4. Theme toggle component:
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<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>
)
}
Vite / Other Frameworks
Use similar approach with next-themes or implement custom solution:
// Store preference
function toggleDarkMode() {
const isDark = document.documentElement.classList.toggle('dark')
localStorage.setItem('theme', isDark ? 'dark' : 'light')
}
// Initialize on load
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
}
CSS Variable System
shadcn/ui uses CSS variables for theming. Variables defined in globals.css:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
Color Format
Values use HSL format without hsl() wrapper for better opacity control:
--primary: 222.2 47.4% 11.2%; /* H S L */
Usage in Tailwind:
background: hsl(var(--primary));
background: hsl(var(--primary) / 0.5); /* 50% opacity */
Tailwind Configuration
Map CSS variables to Tailwind utilities:
// tailwind.config.ts
export default {
darkMode: ["class"],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
}
Color Customization
Method 1: Update CSS Variables
Change colors by modifying CSS variables in globals.css:
:root {
--primary: 262.1 83.3% 57.8%; /* Purple */
--primary-foreground: 210 20% 98%;
}
.dark {
--primary: 263.4 70% 50.4%; /* Darker purple */
--primary-foreground: 210 20% 98%;
}
Method 2: Theme Generator
Use shadcn/ui theme generator: https://ui.shadcn.com/themes
Select base color, generate theme, copy CSS variables.
Method 3: Multiple Themes
Create theme variants with data attributes:
[data-theme="violet"] {
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
}
[data-theme="rose"] {
--primary: 346.8 77.2% 49.8%;
--primary-foreground: 355.7 100% 97.3%;
}
Apply theme:
<div data-theme="violet">
<Button>Violet theme</Button>
</div>
Component Customization
Components live in your codebase - modify directly.
Customize Variants
// components/ui/button.tsx
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-destructive-foreground",
outline: "border border-input bg-background",
// Add custom variant
gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
// Add custom size
xl: "h-14 rounded-md px-10 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
Usage:
<Button variant="gradient" size="xl">Custom Button</Button>
Customize Styles
Modify base styles in component:
// components/ui/card.tsx
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow-lg", // Modified
className
)}
{...props}
/>
))
Override with className
Pass additional classes to override:
<Card className="border-2 border-purple-500 shadow-2xl hover:scale-105 transition-transform">
Custom styled card
</Card>
Base Color Presets
shadcn/ui provides base color presets during init:
- Slate: Cool gray tones
- Gray: Neutral gray
- Zinc: Warm gray
- Neutral: Balanced gray
- Stone: Earthy gray
Select during setup or change later by updating CSS variables.
Style Variants
Two component styles available:
- Default: Softer, more rounded
- New York: Sharp, more contrast
Select during init or in components.json:
{
"style": "new-york",
"tailwind": {
"cssVariables": true
}
}
Radius Customization
Control border radius globally:
:root {
--radius: 0.5rem; /* Default */
--radius: 0rem; /* Sharp corners */
--radius: 1rem; /* Rounded */
}
Components use radius variable:
className="rounded-lg" /* Uses var(--radius) */
Best Practices
- Use CSS Variables: Enables runtime theme switching
- Consistent Foreground Colors: Pair each color with appropriate foreground
- Test Both Themes: Verify components in light and dark modes
- Semantic Naming: Use
destructivenotred,mutednotgray - Accessibility: Maintain sufficient color contrast (WCAG AA minimum)
- Component Overrides: Use
classNameprop for one-off customization - Extract Patterns: Create custom variants for repeated customizations