Skip to content

Dark Mode

Overview

The POS system supports dark mode through PrimeVue 4's built-in theming system. Dark mode is toggled by adding or removing the dark CSS class on the <html> element. PrimeVue's Aura theme automatically adapts all component styles when this class is present.

How It Works

PrimeVue Theme Configuration

The theme is configured in apps/web-client/src/main.ts:

typescript
app.use(PrimeVue, {
  theme: {
    preset: posTheme,
    options: {
      darkModeSelector: '.dark',
    },
  },
});

The darkModeSelector: '.dark' tells PrimeVue to apply dark mode styles when the .dark class is present on any ancestor element (applied to <html>).

Custom Theme Preset

The theme preset extends PrimeVue's Aura base theme with blue as the primary color:

typescript
// packages/ui-kit/src/theme/index.ts
import { definePreset } from '@primevue/themes';
import Aura from '@primevue/themes/aura';

export const posTheme = definePreset(Aura, {
  semantic: {
    primary: {
      50: '{blue.50}',
      100: '{blue.100}',
      // ... all shades
      950: '{blue.950}',
    },
  },
});

useTheme Composable

The useTheme composable manages the dark mode state:

typescript
// apps/web-client/src/composables/useTheme.ts
import { ref, watch } from 'vue';

const STORAGE_KEY = 'pos-theme';
type Theme = 'light' | 'dark';

const currentTheme = ref<Theme>(
  (localStorage.getItem(STORAGE_KEY) as Theme) || 'light'
);

function applyTheme(theme: Theme) {
  document.documentElement.classList.toggle('dark', theme === 'dark');
}

// Apply on load
applyTheme(currentTheme.value);

watch(currentTheme, (val) => {
  localStorage.setItem(STORAGE_KEY, val);
  applyTheme(val);
});

export function useTheme() {
  function toggle() {
    currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light';
  }

  return {
    theme: currentTheme,
    isDark: () => currentTheme.value === 'dark',
    toggle,
  };
}

Key behaviors:

  • Theme preference persists in localStorage under the key pos-theme
  • Default is light mode
  • The .dark class is toggled on document.documentElement (the <html> element)
  • The composable uses a module-level ref, so theme state is shared across all components that import it

Toggle Button

Layouts include a theme toggle button, typically in the header:

vue
<script setup>
import { useTheme } from '@/composables/useTheme';
const { isDark, toggle } = useTheme();
</script>

<template>
  <Button
    :icon="isDark() ? 'pi pi-sun' : 'pi pi-moon'"
    @click="toggle"
    text
    rounded
  />
</template>

CSS Variables for Custom Styling

When writing custom styles that need to respect dark mode, use PrimeVue's CSS custom properties instead of hardcoded colors. PrimeVue automatically updates these variables when dark mode is toggled.

VariableLight ValueDark ValueUse For
--p-content-backgroundwhitedark grayPage/card backgrounds
--p-text-colordark graylight grayPrimary text
--p-text-muted-colormedium graymedium graySecondary text
--p-content-border-colorlight graydark grayBorders
--p-highlight-backgroundlight bluedark blueSelected/active items
--p-highlight-colorbluelight blueSelected item text
--p-surface-0 through --p-surface-950white to blackblack to whiteSurface shades
--p-primary-colorblue-500blue-400Primary action color
--p-primary-contrast-colorwhitewhiteText on primary background

Usage Examples

css
/* Background that adapts to dark mode */
.my-card {
  background: var(--p-content-background);
  color: var(--p-text-color);
  border: 1px solid var(--p-content-border-color);
}

/* Colored background that works in both modes */
.status-badge {
  background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
  color: var(--p-primary-color);
}

/* Hover state */
.my-item:hover {
  background: var(--p-highlight-background);
  color: var(--p-highlight-color);
}

Using color-mix for Colored Backgrounds

When you need a tinted background (e.g., a light blue card on white, dark blue card on dark), use CSS color-mix with a PrimeVue variable:

css
/* 15% of primary color mixed with transparent = subtle tint in both modes */
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);

/* For semantic colors */
background: color-mix(in srgb, var(--p-green-500) 10%, transparent);

This technique works in both light and dark modes because color-mix computes the result relative to the current value of the CSS variable.

Rules

Do

  • Use PrimeVue CSS variables (--p-*) for all colors
  • Use color-mix() for tinted/subtle colored backgrounds
  • Test both light and dark modes when adding new views
  • Use PrimeVue's semantic tokens (--p-content-*, --p-text-*, --p-highlight-*)

Do Not

  • Do not use hardcoded color values (#ffffff, rgb(0,0,0))
  • Do not use PrimeVue's primitive color tokens directly (--p-blue-500). Use semantic tokens (--p-primary-color) that automatically adapt to the theme
  • Do not add separate .dark CSS rules when a CSS variable would work
  • Do not use Tailwind's dark: variant (the project uses PrimeVue's theming system, not Tailwind's dark mode)

Exception

Primitive tokens (--p-blue-500, --p-green-500) are acceptable in color-mix() for status indicators where the color has semantic meaning independent of the theme (e.g., green for success, red for error).