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

TokenPurpose
--haze-primaryBrand color for buttons, links, active states, chart accents
--haze-primary-lighterTinted backgrounds for badges, tips, and highlights
--haze-primary-darkerHover states, gradient endpoints
--haze-bgPage background color
--haze-surfaceCard and panel background
--haze-surface-hoverHover state for surface elements
--haze-text-primaryMain body text
--haze-text-secondaryDescriptions, labels, muted text
--haze-dividerBorder color for separators and table rows
--haze-sidebar-bgSidebar background (dark charcoal in both modes)
--haze-header-bgHeader 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:

TokenUsage
--shadow-cardDefault card elevation (subtle dual-layer shadow)
--shadow-card-hoverElevated hover state (deeper shadow)
--shadow-dropdownDropdown menus and popovers
--shadow-dialogModal 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:

PresetHex
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:

DensityScaleUse Case
Compact0.85Dense data views, fitting more content on screen
Default1.0Standard spacing for most use cases
Spacious1.15Relaxed 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.