Theming

How Theming Works

Understanding the tweakcn/theme-picker theme system: OKLCH colors, CSS variables, light/dark modes, and custom typography.

Theme Naming Convention

Each theme has two variants: light and dark. The theme value follows this pattern:

[theme-name]-light
[theme-name]-dark
Examples:
catppuccin-light // Catppuccin light mode
catppuccin-dark // Catppuccin dark mode
claude-light // Claude light mode
claude-dark // Claude dark mode

OKLCH Color System

tweakcn/theme-picker uses the OKLCH color space for all theme colors. OKLCH provides perceptually uniform colors, meaning colors with the same lightness value actually appear equally bright.

Format:oklch(L C H)
L (Lightness):0 to 1 (0 = black, 1 = white)
C (Chroma):0 to ~0.4 (saturation/colorfulness)
H (Hue):0 to 360 degrees (color wheel)
Examplecss
1/* Primary color in OKLCH */
2--primary: oklch(0.55 0.25 297.02); /* L=0.55, C=0.25, H=297° */
3--primary-foreground: oklch(1 0 0); /* Pure white */
4
5/* The same hue with different lightness */
6--primary-light: oklch(0.75 0.18 297); /* Lighter version */
7--primary-dark: oklch(0.35 0.20 297); /* Darker version */

CSS Variables

Themes define a comprehensive set of CSS custom properties that shadcn/ui components use:

Color Variables

  • --background / --foreground
  • --card / --card-foreground
  • --popover / --popover-foreground
  • --primary / --primary-foreground
  • --secondary / --secondary-foreground
  • --muted / --muted-foreground
  • --accent / --accent-foreground
  • --destructive / --destructive-foreground
  • --border, --input, --ring

Other Variables

  • --radius - Border radius base
  • --font-sans - Sans-serif font stack
  • --font-serif - Serif font stack
  • --font-mono - Monospace font stack
  • --shadow-* - Shadow definitions
  • --chart-1 to --chart-5
  • --sidebar-* - Sidebar-specific colors

Light and Dark Mode

Each theme defines variables for both modes using CSS selectors:

catppuccin.csscss
1/* Light mode - using data-theme attribute */
2[data-theme="catppuccin-light"] {
3 --background: oklch(0.96 0.01 264.53);
4 --foreground: oklch(0.44 0.04 279.33);
5 --primary: oklch(0.55 0.25 297.02);
6 /* ... more variables */
7}
8
9/* Dark mode */
10[data-theme="catppuccin-dark"] {
11 --background: oklch(0.22 0.03 284.06);
12 --foreground: oklch(0.88 0.04 272.28);
13 --primary: oklch(0.79 0.12 304.77);
14 /* ... more variables */
15}

Light Mode

Higher lightness values for backgrounds, lower for text. Subtle shadows and borders.

Dark Mode

Lower lightness for backgrounds, higher for text. Often more saturated accent colors.

Typography

Each theme includes carefully selected Google Fonts that complement the color scheme:

Theme Typographycss
1/* Font variables in theme CSS */
2--font-sans: Montserrat, sans-serif;
3--font-serif: Georgia, serif;
4--font-mono: Fira Code, monospace;
5
6/* Applied in Tailwind */
7body {
8 font-family: var(--font-sans);
9}

Loading Google Fonts

Make sure to load the required fonts. Add this to your layout:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">

Switching Themes Programmatically

Use the useTheme hook from next-themes to control themes programmatically in your components.

The useTheme Hook

Importing the hooktsx
1import { useTheme } from "next-themes"

Return Values

PropertyTypeDescription
themestringCurrent active theme (e.g., "catppuccin-dark")
setTheme(theme: string) => voidFunction to change the current theme
themesstring[]List of all available themes
resolvedThemestringActual theme applied (useful when theme is "system")
systemTheme"light" | "dark"System preference (light or dark)

Basic Usage

Getting the current themetsx
1const { theme, setTheme } = useTheme()
2
3// theme = "catppuccin-dark"
4console.log(theme)

Parsing Theme Values

Since themes follow the [name]-[mode] pattern, you can parse them to get the color theme and mode separately:

Parsing theme name and modetsx
1const { theme } = useTheme()
2
3// Extract the color theme name
4const colorTheme = theme?.replace("-light", "").replace("-dark", "")
5// colorTheme = "catppuccin"
6
7// Check if dark mode
8const isDark = theme?.endsWith("-dark")
9// isDark = true

Setting Themes

Changing themestsx
1const { setTheme } = useTheme()
2
3// Set a specific theme with mode
4setTheme("claude-dark")
5setTheme("vercel-light")
6setTheme("catppuccin-dark")
7
8// Set theme dynamically
9const themeName = "cyberpunk"
10const mode = "dark"
11setTheme(`${themeName}-${mode}`)

Toggling Light/Dark Mode

To toggle between light and dark while keeping the same color theme:

Toggle mode functiontsx
1const { theme, setTheme } = useTheme()
2
3const toggleMode = () => {
4 // Get current color theme (without -light/-dark suffix)
5 const colorTheme = theme?.replace("-light", "").replace("-dark", "")
6
7 // Check current mode
8 const isDark = theme?.endsWith("-dark")
9
10 // Toggle to opposite mode
11 setTheme(`${colorTheme}-${isDark ? "light" : "dark"}`)
12}
13
14// If theme is "catppuccin-dark", calling toggleMode()
15// will change it to "catppuccin-light"

Switching Color Themes

To switch to a different color theme while preserving the current mode:

Switch color theme functiontsx
1const { theme, setTheme } = useTheme()
2
3const switchColorTheme = (newTheme: string) => {
4 // Keep the current mode (light/dark)
5 const isDark = theme?.endsWith("-dark")
6
7 // Apply new theme with same mode
8 setTheme(`${newTheme}-${isDark ? "dark" : "light"}`)
9}
10
11// Usage
12switchColorTheme("claude") // Switches to claude-[current-mode]
13switchColorTheme("vercel") // Switches to vercel-[current-mode]
14switchColorTheme("cyberpunk") // Switches to cyberpunk-[current-mode]

Getting Available Themes

You can also import the theme configuration directly:

Using themes configtsx
1import { themes, sortedThemes, allThemeValues } from "@/lib/themes-config"
2
3// themes - Array of theme objects with metadata
4themes.forEach(t => {
5 console.log(t.name) // "catppuccin"
6 console.log(t.title) // "Catppuccin"
7 console.log(t.description) // "Soothing pastel theme..."
8 console.log(t.category) // "colorful"
9})
10
11// sortedThemes - Alphabetically sorted (default first)
12// allThemeValues - All theme strings: ["default-light", "default-dark", ...]

Hydration Warning

The theme is stored in localStorage and isn't available during SSR. Always check if the component is mounted before rendering theme-dependent UI:

const [mounted, setMounted] = useState(false)
const { theme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
// Avoid hydration mismatch
if (!mounted) return null
// Safe to use theme now
return <div>Current: {theme}</div>