Expert guidance for creating, modifying, and managing themes in the Dashboard application.
Themes use CSS Variables + Data Attributes with separate files per theme:
src/themes/
├── index.ts # Theme registry, types, and utilities
├── default.css # Neutral grayscale theme
├── ocean.css # Blue tones
├── forest.css # Green/brown tones
└── sunset.css # Orange/pink tones
Additional files:
src/hooks/use-theme.ts — React hook for theme state managementsrc/components/theme-picker.tsx — UI component for theme selectionThemes are imported in globals.css and applied via [data-theme="name"] selector.
| Concern | Mechanism | Storage Key |
|---|
| Color Scheme (light/dark) | .dark class on <html> | localStorage.theme |
| Theme Palette (ocean, forest, etc.) | data-theme attribute on <html> | localStorage.theme-name + DB profiles.theme |
Create src/themes/{name}.css:
/* Theme: {Name}
* {One-line description}
*/
/* Light mode */
[data-theme="{name}"] {
--primary: oklch(...);
/* ... all 28 variables */
}
/* Dark mode */
[data-theme="{name}"].dark {
--primary: oklch(...);
/* ... all 28 variables */
}
@import "../themes/{name}.css";
Update src/themes/index.ts:
export const THEMES = ["default", "ocean", "forest", "sunset", "{name}"] as const;
// Add to themeRegistry array
{
name: "{name}",
label: "{Display Name}",
description: "{Brief description}",
},
Create a new migration in supabase/migrations/:
ALTER TABLE public.profiles
DROP CONSTRAINT IF EXISTS profiles_theme_check;
ALTER TABLE public.profiles
ADD CONSTRAINT profiles_theme_check
CHECK (theme IN ('default', 'ocean', 'forest', 'sunset', '{name}'));
Push with: npx supabase db push --yes
Update the colors object in ThemePreview component in theme-picker.tsx:
const colors: Record<ThemeName, { primary: string; accent: string; bg: string }> = {
// ... existing themes
{name}: { primary: "#hex", accent: "#hex", bg: "#hex" },
};
Each theme must define all variables for both light and dark modes:
--background, --foreground--card, --card-foreground--popover, --popover-foreground--primary, --primary-foreground--secondary, --secondary-foreground--muted, --muted-foreground--accent, --accent-foreground--destructive--border, --input, --ring--chart-1 through --chart-5--sidebar, --sidebar-foreground--sidebar-primary, --sidebar-primary-foreground--sidebar-accent, --sidebar-accent-foreground--sidebar-border, --sidebar-ringAll colors use OKLCH format: oklch(lightness chroma hue)
| Component | Range | Notes |
|---|---|---|
| Lightness | 0-1 | 0 = black, 1 = white |
| Chroma | 0-0.4 | 0 = gray, higher = more saturated |
| Hue | 0-360 | Color wheel angle |
globals.css loads all theme CSS filesuseTheme hook reads localStorage.theme-namedata-theme attribute on <html>const { theme, setTheme } = useTheme();
// theme: ThemeName — current theme ("default" | "ocean" | "forest" | "sunset")
// setTheme: (theme: ThemeName) => void — change theme (updates localStorage + DOM)
<ThemePicker
defaultValue={profile?.theme} // Server-side initial value
name="theme" // Form field name for submission
/>
Features:
Verify each theme in both light and dark modes:
| File | Change |
|---|---|
src/themes/{name}.css | Create new theme file |
src/app/globals.css | Add import statement |
src/themes/index.ts | Add to THEMES array and themeRegistry |
src/components/theme-picker.tsx | Add preview colors |
supabase/migrations/ | New migration for DB constraint |