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:
: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-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
| 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 Tailwind 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 two systems working together:
1. Nuxt UI colors — Set in app/app.config.ts. This controls all Nuxt UI components (buttons, inputs, badges):
// 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:
/* 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:
<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:
| Preset | Color | Hex |
|---|---|---|
| Teal (default) | teal | #14b8a6 |
| Blue | blue | #3b82f6 |
| Purple | purple | #8b5cf6 |
| Orange | orange | #f97316 |
| Rose | rose | #f43f5e |
| Emerald | emerald | #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.
<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:
| 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. 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:
// 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.