Skip to content

Customization & Theming

Transform your React admin template to match your brand identity with advanced theming capabilities, dynamic color systems, and comprehensive customization options.

Tailwind CSS 4 CSS Variables shadcn/ui Zustand State

The React template uses a sophisticated theming system that combines CSS custom properties, Zustand state management, and Tailwind CSS 4 for dynamic theme switching and brand customization.

CSS Variables

OKLCH color space variables for better color manipulation and accessibility

Zustand Store

Reactive state management for theme preferences with persistence

Dynamic Switching

Real-time theme updates without page refreshes

The template includes 8 pre-built color themes with both light and dark variants:

src/constant/themeColors.ts
// Available theme colors with OKLCH values
export const THEME_COLORS: ThemeColorConfig[] = [
{
name: 'indigo',
color: 'oklch(58.5% 0.233 277.117)', // Light mode
darkColor: 'oklch(67.3% 0.182 276.935)', // Dark mode
class: 'bg-indigo-500',
darkClass: 'bg-indigo-400',
},
{
name: 'amber',
color: 'oklch(76.9% 0.188 70.08)',
darkColor: 'oklch(82.8% 0.189 84.429)',
class: 'bg-amber-500',
darkClass: 'bg-amber-400',
},
// Additional colors: emerald, sky, purple, pink, stone, rose
]

The theme color state is managed using Zustand with localStorage persistence:

src/store/themeColorStore.ts
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface ThemeColorState {
themeColor: string
setThemeColor: (color: string) => void
}
export const useThemeColorStore = create<ThemeColorState>()(
persist(
(set) => ({
themeColor: 'indigo', // Default theme color
setThemeColor: (color) => set({ themeColor: color }),
}),
{
name: 'theme-color-storage',
storage: createJSONStorage(() => localStorage),
}
)
)

The useThemeColor hook provides easy access to theme color functionality:

src/hooks/useThemeColor.ts
import { useCallback } from 'react'
import { useTheme } from '@/store'
import { useThemeColorStore } from '@/store/themeColorStore'
import { THEME_COLORS, type ThemeColorConfig } from '@/constant/themeColors'
export function useThemeColor() {
const { resolvedTheme } = useTheme()
const { themeColor, setThemeColor: setThemeColorStore } = useThemeColorStore()
const isDark = resolvedTheme === 'dark'
const setThemeColor = useCallback((themeColorName: string) => {
const item = THEME_COLORS.find((item) => item.name === themeColorName)
if (item) {
setThemeColorStore(themeColorName)
const root = document.documentElement
// Update CSS custom properties
root.style.setProperty('--primary', isDark ? item.darkColor : item.color)
root.style.setProperty('--sidebar-primary', isDark ? item.darkColor : item.color)
}
}, [isDark, setThemeColorStore])
return {
themeColor,
setThemeColor,
themeColorList: THEME_COLORS,
}
}

The template includes a comprehensive theme system with system preference detection:

src/store/themeStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
type Theme = "dark" | "light" | "system"
interface ThemeState {
theme: Theme
setTheme: (theme: Theme) => void
resolvedTheme: "dark" | "light"
initialize: () => void
}
const getSystemTheme = (): "dark" | "light" => {
if (typeof window === 'undefined') return 'light'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const applyTheme = (theme: Theme) => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
const resolvedTheme = theme === "system" ? getSystemTheme() : theme
root.classList.add(resolvedTheme)
return resolvedTheme
}
export const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
theme: "system",
resolvedTheme: "light",
setTheme: (theme: Theme) => {
const resolvedTheme = applyTheme(theme)
set({ theme, resolvedTheme })
},
initialize: () => {
const { theme } = get()
const resolvedTheme = applyTheme(theme)
set({ resolvedTheme })
// Listen for system theme changes
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = () => {
const currentTheme = get().theme
if (currentTheme === 'system') {
const newResolvedTheme = getSystemTheme()
const root = window.document.documentElement
root.classList.remove("light", "dark")
root.classList.add(newResolvedTheme)
set({ resolvedTheme: newResolvedTheme })
}
}
mediaQuery.addEventListener('change', handleChange)
}
},
}),
{
name: 'admin-theme',
partialize: (state) => ({ theme: state.theme }),
}
)
)

The template uses a comprehensive CSS variable system with OKLCH color values:

src/assets/styles/index.css
:root {
--radius: 0.65rem;
/* Light theme colors */
--background: oklch(1 0 0);
--foreground: oklch(21% 0.034 264.665);
--primary: oklch(58.5% 0.233 277.117);
--primary-foreground: oklch(0.969 0.016 293.756);
--secondary: oklch(0.967 0.001 286.375);
--muted: oklch(96.7% 0.003 264.542);
--border: oklch(92.8% 0.006 264.531 / 40%);
/* Sidebar specific variables */
--sidebar: oklch(1 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(58.5% 0.233 277.117);
--sidebar-border: oklch(87.2% 0.01 258.338 / 25%);
}
.dark {
/* Dark theme colors */
--background: oklch(13% 0.028 261.692);
--foreground: oklch(98.5% 0.002 247.839);
--primary: oklch(67.3% 0.182 276.935);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-primary: oklch(67.3% 0.182 276.935);
/* Additional dark theme variables... */
}

The template integrates CSS variables with Tailwind CSS through custom theme configuration:

src/assets/styles/index.css
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-sidebar: var(--sidebar);
--color-sidebar-primary: var(--sidebar-primary);
/* Additional color mappings... */
}

The template includes a comprehensive settings panel for theme customization:

src/layouts/components/SettingSheet.tsx
import { Sun, Moon, Monitor, Palette, Check, Sidebar, NavigationOff } from 'lucide-react'
import { Icon } from '@iconify/react'
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
import { Switch } from '@/components/ui/switch'
import { motion } from 'framer-motion'
import { useTheme } from '@/store'
import { useThemeColor, type ThemeColorInterface } from '@/hooks/useThemeColor'
import { useUIStore } from '@/store/uiStore'
import { cn } from '@/lib/utils'
export function SettingSheet() {
const { setTheme, resolvedTheme } = useTheme()
const { themeColor, themeColorList, setThemeColor } = useThemeColor()
const { layoutMode, setLayoutMode } = useUIStore()
const isDark = resolvedTheme === 'dark'
const handleChangeThemeMode = (checked: boolean) => {
document.documentElement.classList.add('theme-switching')
setTheme(checked ? 'dark' : 'light')
setTimeout(() => {
document.documentElement.classList.remove('theme-switching')
}, 0)
}
const handleChangeThemeColor = (item: ThemeColorInterface) => {
setThemeColor(item.name)
}
const handleChangeLayoutMode = (mode: 'sidebar' | 'topbar') => {
setLayoutMode(mode)
}
return (
<Sheet>
<SheetTrigger asChild>
<motion.div
className="scale-100 hover:scale-105 p-2"
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, repeatType: 'loop', duration: 15, ease: 'linear' }}
>
<Icon className="text-xl text-gray-400/70" icon="icon-park-solid:setting" />
</motion.div>
</SheetTrigger>
<SheetContent className="w-96 flex flex-col sm:w-[400px]">
<SheetHeader className="pb-6">
<SheetTitle className="text-xl font-semibold flex items-center gap-2">
<Icon icon="lucide:palette" className="text-primary" />
Customize
</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-6 pb-6 px-5">
{/* Theme Mode Section */}
<motion.div className="space-y-4" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
<Monitor className="w-4 h-4 text-primary" />
Appearance
</h3>
</div>
<motion.div
className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border hover:bg-muted/50"
whileHover={{ scale: 1.01 }}
>
<div className="flex items-center gap-3">
<motion.div
animate={{ rotate: isDark ? 180 : 0 }}
className="p-2 rounded-full bg-background shadow-sm"
>
{isDark ? <Moon className="w-4 h-4 text-primary" /> : <Sun className="w-4 h-4 text-primary" />}
</motion.div>
<span className="text-sm font-medium">{isDark ? 'Dark Mode' : 'Light Mode'}</span>
</div>
<Switch checked={isDark} onCheckedChange={handleChangeThemeMode} />
</motion.div>
</motion.div>
{/* Layout Mode Section */}
<motion.div className="space-y-4">
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
<Monitor className="w-4 h-4 text-primary" />
Layout Mode
</h3>
<div className="grid gap-3 grid-cols-2">
{/* Sidebar Layout */}
<motion.div
className={cn(
'group relative flex flex-col items-center justify-center p-4 rounded-lg border-2 cursor-pointer',
layoutMode === 'sidebar'
? 'border-primary bg-primary/5 text-primary'
: 'border-border hover:border-border/60'
)}
onClick={() => handleChangeLayoutMode('sidebar')}
whileHover={{ scale: 1.02 }}
>
<Sidebar className="w-6 h-6 mb-2" />
<span className="text-xs font-medium">Sidebar</span>
{layoutMode === 'sidebar' && (
<motion.div className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</motion.div>
)}
</motion.div>
{/* Topbar Layout */}
<motion.div
className={cn(
'group relative flex flex-col items-center justify-center p-4 rounded-lg border-2 cursor-pointer',
layoutMode === 'topbar'
? 'border-primary bg-primary/5 text-primary'
: 'border-border hover:border-border/60'
)}
onClick={() => handleChangeLayoutMode('topbar')}
whileHover={{ scale: 1.02 }}
>
<NavigationOff className="w-6 h-6 mb-2" />
<span className="text-xs font-medium">Topbar</span>
{layoutMode === 'topbar' && (
<motion.div className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</motion.div>
)}
</motion.div>
</div>
</motion.div>
{/* Color Theme Section */}
<motion.div className="space-y-4">
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
<Palette className="w-4 h-4 text-primary" />
Color Theme
</h3>
<div className="grid grid-cols-4 gap-3">
{themeColorList.map((item, index) => (
<motion.div
key={item.name}
className={cn(
'group relative flex items-center justify-center h-16 rounded-xl border-2 cursor-pointer',
themeColor === item.name
? 'border-primary shadow-lg shadow-primary/25'
: 'border-border hover:border-border/60'
)}
onClick={() => handleChangeThemeColor(item)}
whileHover={{ scale: 1.05, y: -2 }}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4 + index * 0.05 }}
>
<motion.div
animate={{ scale: themeColor === item.name ? 1.2 : 1 }}
className={cn(
'relative flex items-center justify-center rounded-full shadow-lg',
isDark ? item.darkClass : item.class,
themeColor === item.name ? 'w-8 h-8' : 'w-6 h-6'
)}
>
{themeColor === item.name && (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }}>
<Check className="w-4 h-4 text-white" />
</motion.div>
)}
</motion.div>
{/* Tooltip */}
<div className="absolute -bottom-8 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-popover border rounded-md text-xs opacity-0 group-hover:opacity-100">
{item.name}
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</SheetContent>
</Sheet>
)
}

To add custom color themes, extend the THEME_COLORS array:

src/constant/themeColors.ts
// Add your custom theme color
export const THEME_COLORS: ThemeColorConfig[] = [
// ... existing colors
{
name: 'custom-brand',
color: 'oklch(65% 0.2 280)', // Your brand color in light mode
darkColor: 'oklch(70% 0.15 280)', // Your brand color in dark mode
class: 'bg-blue-600', // Tailwind class for light mode
darkClass: 'bg-blue-500', // Tailwind class for dark mode
}
]

Add custom CSS variables for specific use cases:

src/assets/styles/custom.css
:root {
/* Your custom variables */
--brand-primary: oklch(65% 0.2 280);
--brand-secondary: oklch(70% 0.15 120);
--custom-gradient: linear-gradient(135deg, var(--brand-primary), var(--brand-secondary));
}
.dark {
--brand-primary: oklch(70% 0.15 280);
--brand-secondary: oklch(75% 0.12 120);
}
/* Custom utility classes */
.bg-brand-primary {
background-color: var(--brand-primary);
}
.text-brand-primary {
color: var(--brand-primary);
}

Override shadcn/ui component styles using CSS variables and Tailwind classes:

src/assets/styles/components.css
/* Override Button component */
.btn-custom {
@apply bg-brand-primary hover:bg-brand-primary/90;
@apply border-brand-primary text-white;
@apply shadow-lg hover:shadow-xl;
transition: all 0.2s ease-in-out;
}
/* Override Card component */
.card-custom {
@apply bg-gradient-to-br from-background to-muted/50;
@apply border-border/50 backdrop-blur-sm;
@apply hover:shadow-lg transition-shadow duration-300;
}

Create dynamic theme variables that respond to user interactions:

src/utils/themeUtils.ts
// Utility for dynamic color generation
export function generateThemeVariations(baseColor: string) {
const variations = {
50: adjustOklchLightness(baseColor, 0.95),
100: adjustOklchLightness(baseColor, 0.9),
500: baseColor,
900: adjustOklchLightness(baseColor, 0.2),
}
return variations
}
// Apply dynamic colors to CSS variables
export function applyDynamicTheme(colorName: string, baseColor: string) {
const root = document.documentElement
const variations = generateThemeVariations(baseColor)
Object.entries(variations).forEach(([shade, color]) => {
root.style.setProperty(`--color-${colorName}-${shade}`, color)
})
}

Color Accessibility

Ensure sufficient contrast ratios (4.5:1 for normal text, 3:1 for large text) when customizing colors

Performance

Use CSS custom properties for theme switching instead of class-based approaches for better performance

Consistency

Maintain consistent color naming and spacing across your custom theme system

Testing

Test your custom themes in both light and dark modes across different devices

  1. Theme not applying: Ensure CSS variables are properly defined and the theme store is initialized
  2. Flash of unstyled content: Initialize the theme system before rendering components
  3. Color inconsistency: Verify OKLCH values are correctly formatted and accessible

Enable theme debugging for development:

src/utils/debug.ts
// Add to your development environment
if (process.env.NODE_ENV === 'development') {
window.debugTheme = {
currentTheme: () => useThemeStore.getState().theme,
currentColor: () => useThemeColorStore.getState().themeColor,
cssVars: () => {
const styles = getComputedStyle(document.documentElement)
return {
primary: styles.getPropertyValue('--primary'),
background: styles.getPropertyValue('--background'),
}
}
}
}

Transform your React admin template into a unique, branded experience that perfectly matches your design requirements! 🎨