Customization
Theming
Learn how the design system works and how to customize colors, dark mode, density, and direction.
Design System Overview
Haze Dashboard uses a Minimals-inspired design system built on OKLCH color tokens, solid card surfaces with layered shadow elevation, and a dark charcoal sidebar.
All design decisions flow from CSS custom properties defined in src/app/globals.css. Changing a single token updates every component that references it — buttons, cards, charts, badges, and sidebar accents all respond automatically.
CSS Custom Properties
The design tokens are defined in two blocks: :root for light mode and .dark for dark mode. Here is the full light mode token set:
:root {
--haze-primary: oklch(0.58 0.17 170);
--haze-primary-lighter: oklch(0.92 0.04 170);
--haze-primary-darker: oklch(0.42 0.14 170);
--haze-bg: #F9FAFB;
--haze-surface: #FFFFFF;
--haze-surface-hover: #F4F6F8;
--haze-text-primary: #1C252E;
--haze-text-secondary: #637381;
--haze-text-disabled: #919EAB;
--haze-divider: rgba(145, 158, 171, 0.2);
--haze-sidebar-bg: #1C252E;
--haze-sidebar-text: #919EAB;
--haze-sidebar-text-active: #FFFFFF;
--haze-sidebar-active-bg: rgba(0, 167, 111, 0.08);
--haze-header-bg: rgba(255, 255, 255, 0.8);
}Dark Mode Tokens
When dark mode is active, the .dark class on the <html> element overrides every token:
.dark {
--haze-primary: oklch(0.65 0.17 170);
--haze-primary-lighter: oklch(0.25 0.06 170);
--haze-primary-darker: oklch(0.50 0.14 170);
--haze-bg: #141A21;
--haze-surface: #1C252E;
--haze-surface-hover: #252F3A;
--haze-text-primary: #FFFFFF;
--haze-text-secondary: #919EAB;
--haze-text-disabled: #637381;
--haze-divider: rgba(145, 158, 171, 0.16);
--haze-sidebar-bg: #1C252E;
--haze-header-bg: rgba(20, 26, 33, 0.8);
}Semantic Token Reference
| Token | Purpose |
|---|---|
| --haze-primary | Brand color for buttons, links, active states, chart accents |
| --haze-primary-lighter | Tinted backgrounds for badges, tips, and highlights |
| --haze-primary-darker | Hover states, gradient endpoints |
| --haze-bg | Page background color |
| --haze-surface | Card and panel background |
| --haze-surface-hover | Hover state for surface elements |
| --haze-text-primary | Main body text |
| --haze-text-secondary | Descriptions, labels, muted text |
| --haze-divider | Border color for separators and table rows |
| --haze-sidebar-bg | Sidebar background (dark charcoal in both modes) |
| --haze-header-bg | Header background with backdrop blur transparency |
Shadow Elevation System
Cards and surfaces use a layered shadow system defined as theme tokens. Shadows transition smoothly on hover to create a sense of depth:
| Token | Usage |
|---|---|
| --shadow-card | Default card elevation (subtle dual-layer shadow) |
| --shadow-card-hover | Elevated hover state (deeper shadow) |
| --shadow-dropdown | Dropdown menus and popovers |
| --shadow-dialog | Modal dialogs (dramatic offset shadow) |
Changing the Primary Color
The primary color is controlled by the CSS custom properties in src/app/globals.css. To switch from teal to blue, change the OKLCH hue from 170 to 250:
/* Change hue from 170 (teal) to 250 (blue) */
:root {
--haze-primary: oklch(0.58 0.17 250);
--haze-primary-lighter: oklch(0.92 0.04 250);
--haze-primary-darker: oklch(0.42 0.14 250);
}
.dark {
--haze-primary: oklch(0.65 0.17 250);
--haze-primary-lighter: oklch(0.25 0.06 250);
--haze-primary-darker: oklch(0.50 0.14 250);
}Runtime Color Switching
The useThemeSettings hook (a Zustand store) lets users switch accent colors at runtime without a page reload. Settings persist to localStorage:
'use client'
import { useThemeSettings } from '@/hooks/use-theme-settings'
export function ThemeControls() {
const setAccentColor = useThemeSettings(s => s.setAccentColor)
const setDensity = useThemeSettings(s => s.setDensity)
const setRtl = useThemeSettings(s => s.setRtl)
return (
<>
<button onClick={() => setAccentColor('blue')}>Blue</button>
<button onClick={() => setDensity('compact')}>Compact</button>
<button onClick={() => setRtl(true)}>RTL on</button>
</>
)
}Accent Color Presets
The theme customizer offers 6 accent color presets:
| Preset | Hex |
|---|---|
| Teal (default) | #14b8a6 |
| Blue | #3b82f6 |
| Purple | #8b5cf6 |
| Orange | #f97316 |
| Rose | #f43f5e |
| Emerald | #10b981 |
Dark Mode
Dark mode is managed by the same theme settings hook. It supports three states: light, dark, and system (follows OS preference). The preference persists to localStorage.
'use client'
import { useThemeSettings } from '@/hooks/use-theme-settings'
import { Sun, Moon } from 'lucide-react'
export function DarkModeToggle() {
const colorMode = useThemeSettings(s => s.colorMode)
const setColorMode = useThemeSettings(s => s.setColorMode)
return (
<button
onClick={() => setColorMode(colorMode === 'dark' ? 'light' : 'dark')}
aria-label="Toggle color mode"
>
{colorMode === 'dark' ? <Sun /> : <Moon />}
</button>
)
}When dark mode is active, the .dark class is added to <html>, and all --haze-* tokens switch to their dark values.
Density
Density controls global spacing via a CSS custom property --haze-density-scale. Three levels are available:
| Density | Scale | Use Case |
|---|---|---|
| Compact | 0.85 | Dense data views, fitting more content on screen |
| Default | 1.0 | Standard spacing for most use cases |
| Spacious | 1.15 | Relaxed layout with more breathing room |
RTL Support
The entire dashboard supports right-to-left text direction for Arabic, Hebrew, Persian, and other RTL languages. When enabled, the theme hook sets dir="rtl" on the <html> element.
Tailwind's logical properties handle most directional flipping automatically: ms-* / me-* for margins, ps-* / pe-* for padding, text-start / text-end for alignment. Use ltr: and rtl: variants for edge cases.
Tip
All theme settings (accent color, density, RTL) persist to localStorage via Zustand's built-in persist middleware. They survive page refreshes and browser restarts automatically.
Next Steps
See the Charts guide for data visualization, or browse the Components reference.