374 lines
8.5 KiB
Markdown
374 lines
8.5 KiB
Markdown
# shadcn/ui Theming & Customization
|
|
|
|
Theme configuration, CSS variables, dark mode, and component customization.
|
|
|
|
## Dark Mode Setup
|
|
|
|
### Next.js App Router
|
|
|
|
**1. Install next-themes:**
|
|
```bash
|
|
npm install next-themes
|
|
```
|
|
|
|
**2. Create theme provider:**
|
|
```tsx
|
|
// 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:**
|
|
```tsx
|
|
// 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:**
|
|
```tsx
|
|
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:
|
|
|
|
```javascript
|
|
// 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`:
|
|
|
|
```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:
|
|
```css
|
|
--primary: 222.2 47.4% 11.2%; /* H S L */
|
|
```
|
|
|
|
Usage in Tailwind:
|
|
```css
|
|
background: hsl(var(--primary));
|
|
background: hsl(var(--primary) / 0.5); /* 50% opacity */
|
|
```
|
|
|
|
## Tailwind Configuration
|
|
|
|
Map CSS variables to Tailwind utilities:
|
|
|
|
```ts
|
|
// 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`:
|
|
|
|
```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:
|
|
|
|
```css
|
|
[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:
|
|
```tsx
|
|
<div data-theme="violet">
|
|
<Button>Violet theme</Button>
|
|
</div>
|
|
```
|
|
|
|
## Component Customization
|
|
|
|
Components live in your codebase - modify directly.
|
|
|
|
### Customize Variants
|
|
|
|
```tsx
|
|
// 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:
|
|
```tsx
|
|
<Button variant="gradient" size="xl">Custom Button</Button>
|
|
```
|
|
|
|
### Customize Styles
|
|
|
|
Modify base styles in component:
|
|
|
|
```tsx
|
|
// 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:
|
|
|
|
```tsx
|
|
<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`:
|
|
|
|
```json
|
|
{
|
|
"style": "new-york",
|
|
"tailwind": {
|
|
"cssVariables": true
|
|
}
|
|
}
|
|
```
|
|
|
|
## Radius Customization
|
|
|
|
Control border radius globally:
|
|
|
|
```css
|
|
:root {
|
|
--radius: 0.5rem; /* Default */
|
|
--radius: 0rem; /* Sharp corners */
|
|
--radius: 1rem; /* Rounded */
|
|
}
|
|
```
|
|
|
|
Components use radius variable:
|
|
```tsx
|
|
className="rounded-lg" /* Uses var(--radius) */
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Use CSS Variables**: Enables runtime theme switching
|
|
2. **Consistent Foreground Colors**: Pair each color with appropriate foreground
|
|
3. **Test Both Themes**: Verify components in light and dark modes
|
|
4. **Semantic Naming**: Use `destructive` not `red`, `muted` not `gray`
|
|
5. **Accessibility**: Maintain sufficient color contrast (WCAG AA minimum)
|
|
6. **Component Overrides**: Use `className` prop for one-off customization
|
|
7. **Extract Patterns**: Create custom variants for repeated customizations
|