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. The system provides a cohesive visual language across 80+ pages while remaining easy to customize.

All design decisions flow from CSS custom properties defined in app/assets/css/main.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:

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

css
.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-sidebar-text: #919EAB;
  --haze-sidebar-text-active: #FFFFFF;
  --haze-sidebar-active-bg: rgba(0, 167, 111, 0.16);
  --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 Tailwind 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 two systems working together:

1. Nuxt UI colors — Set in app/app.config.ts. This controls all Nuxt UI components (buttons, inputs, badges):

typescript
// app/app.config.ts
export default defineAppConfig({
  ui: {
    colors: {
      primary: 'teal',    // Change to 'blue', 'purple', etc.
      secondary: 'slate',
      success: 'emerald',
      warning: 'amber',
      error: 'red',
      info: 'sky',
    },
  },
})

2. Custom CSS tokens — Set in app/assets/css/main.css. This controls custom components (sidebar accents, gradient buttons, chart colors). To switch from teal to blue, change the hue from 170 to 250:

css
/* 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 composable lets users switch accent colors at runtime without a page reload. It updates the Nuxt UI appConfig color palette and persists the choice to localStorage:

typescript
<script setup lang="ts">
const { settings, presets, setAccentColor, setDensity, setRtl } = useThemeSettings()

// Switch to blue accent
setAccentColor('blue')

// Set compact density
setDensity('compact')

// Enable RTL
setRtl(true)
</script>

Accent Color Presets

The theme customizer offers 6 accent color presets. Each preset maps to a Tailwind/Nuxt UI color name:

PresetColorHex
Teal (default)teal #14b8a6
Blueblue #3b82f6
Purplepurple #8b5cf6
Orangeorange #f97316
Roserose #f43f5e
Emeraldemerald #10b981

To add a new preset, add an entry to the accentPresets object in app/composables/useThemeSettings.ts. The color name must match a Tailwind/Nuxt UI color palette name.

Dark Mode

Dark mode is managed by Nuxt UI's built-in color mode module (powered by @nuxtjs/color-mode). It supports three states: light, dark, and system (follows OS preference). The preference persists to localStorage.

vue
<script setup lang="ts">
const colorMode = useColorMode()

function toggleDarkMode() {
  colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
</script>

<template>
  <UButton
    :icon="colorMode.value === 'dark' ? 'i-lucide-sun' : 'i-lucide-moon'"
    variant="ghost"
    @click="toggleDarkMode"
  />
</template>

The toggle is available in the header and the Appearance settings page. 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. The toggle is in the Appearance settings. When enabled, the composable 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 text alignment. For edge cases (icon rotation, absolute positioning), use Tailwind's ltr: and rtl: variants.

Nuxt UI Component Overrides

Nuxt UI components (buttons, inputs, modals, tables) are customized globally in app/app.config.ts via the slot system. This is where cards get the .card class, modals get the glass overlay, and inputs get rounded corners:

typescript
// app/app.config.ts
export default defineAppConfig({
  ui: {
    colors: {
      primary: 'teal',
      secondary: 'slate',
      success: 'emerald',
      warning: 'amber',
      error: 'red',
      info: 'sky',
    },
    card: {
      slots: {
        root: 'card card-hover',          // Custom shadow elevation
        header: 'border-b border-divider',
      },
    },
    button: {
      defaultVariants: { color: 'primary' },
    },
    input: {
      slots: {
        root: 'rounded-[var(--radius-input)]',
      },
    },
    modal: {
      slots: {
        content: 'glass-overlay rounded-[var(--radius-card-lg)]',
        overlay: 'bg-black/50',
      },
    },
  },
})

Tip

All theme settings (accent color, density, RTL) persist to localStorage via @vueuse/core's useLocalStorage. They survive page refreshes and browser restarts automatically.

Next Steps

See the Charts guide to learn about data visualization, or browse the Components reference.