Skip to content

UI Theming System

The UI library uses a modern CSS theming system based on OKLCH colors and CSS custom properties. This provides precise color control, excellent dark mode support, and easy customization.


OKLCH (Oklch) is a perceptually uniform color space that provides:

  • Consistent Lightness: Same perceived brightness across hues
  • Wide Gamut: Access to P3 and Rec. 2020 colors
  • Intuitive Adjustments: Change lightness, chroma, or hue independently
/* OKLCH format: oklch(lightness chroma hue) */
--color-primary: oklch(0.552 0.278 264);
/* L C H */
/* 55.2% 27.8 264° */

Defined in packages/ui/theme.css:

:host {
/* Background & Foreground */
--background: oklch(0.999 0.0017 245);
--foreground: oklch(0.09 0.005 20);
/* Primary - Main accent color */
--primary: oklch(0.552 0.278 264);
--primary-foreground: oklch(0.98 0.005 245);
/* Secondary */
--secondary: oklch(66.532% 0.0383 271.338);
--secondary-foreground: oklch(0.98 0.005 245);
/* Muted - Subtle backgrounds */
--muted: oklch(0.9213 0.0146 240);
--muted-foreground: oklch(0.45 0.015 20);
/* Accent - Highlights */
--accent: oklch(0.92 0.015 245);
--accent-foreground: oklch(0.09 0.005 20);
/* Semantic Colors */
--danger: oklch(0.628 0.257 29.2);
--danger-foreground: oklch(0.98 0.013 17.4);
--warning: oklch(0.845 0.167 83.9);
--warning-foreground: oklch(0.22 0.084 86.1);
--success: oklch(0.7 0.17 142);
--success-foreground: oklch(0.98 0.05 142);
--info: oklch(0.44043 0.16728 263.481);
--info-foreground: oklch(0.97277 0.01784 223.858);
/* Borders & Inputs */
--border: oklch(0.095 0.005 20);
--input: oklch(0.802 0 20);
--ring: oklch(0.09 0.005 20);
}

Dark mode is activated by adding the .dark class:

.dark {
--background: oklch(0.2031 0.0608 268);
--foreground: oklch(0.98 0.013 20);
--primary: oklch(0.602 0.278 264);
--primary-foreground: oklch(0.98 0.005 20);
--secondary: oklch(0.66 0.0383 271.338);
--secondary-foreground: oklch(0.98 0.005 20);
--muted: oklch(0.275 0.0586 268);
--muted-foreground: oklch(0.649 0.015 20);
--accent: oklch(0.36 0.0608 268);
--accent-foreground: oklch(0.98 0.013 20);
--border: oklch(0.9913 0.0178 260);
--input: oklch(0.5013 0.0178 260);
--ring: oklch(0.9613 0.0178 260);
}

The theme variables are mapped to Tailwind classes:

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-danger: var(--danger);
--color-danger-foreground: var(--danger-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
<template>
<!-- Background colors -->
<div class="bg-background text-foreground">Default</div>
<div class="bg-primary text-primary-foreground">Primary</div>
<div class="bg-muted text-muted-foreground">Muted</div>
<!-- Semantic colors -->
<div class="bg-danger text-danger-foreground">Danger</div>
<div class="bg-success text-success-foreground">Success</div>
<div class="bg-warning text-warning-foreground">Warning</div>
<!-- Borders -->
<div class="border border-border">Bordered</div>
<!-- Focus rings -->
<input class="focus:ring-2 focus:ring-ring" />
</template>

ColorPurposeUse For
primaryMain brand/actionPrimary buttons, links, accents
secondarySupportingSecondary actions, less emphasis
mutedSubtleBackgrounds, disabled states
accentHighlightHover states, selections
dangerDestructiveDelete, error states
successPositiveCompletion, valid states
warningCautionWarnings, pending states
infoInformationalTips, neutral info

/* In your custom CSS */
:root {
/* Change primary to a green */
--primary: oklch(0.6 0.2 145);
--primary-foreground: oklch(0.98 0.02 145);
}
.dark {
--primary: oklch(0.65 0.2 145);
}
themes.ts
export const themes = {
default: {
primary: 'oklch(0.552 0.278 264)', // Purple
},
ocean: {
primary: 'oklch(0.6 0.2 220)', // Blue
},
forest: {
primary: 'oklch(0.55 0.18 150)', // Green
},
sunset: {
primary: 'oklch(0.6 0.25 30)', // Orange
},
};
export function applyTheme(themeName: keyof typeof themes) {
const theme = themes[themeName];
const root = document.documentElement;
Object.entries(theme).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
}

<script setup>
import { ref, watchEffect } from 'vue';
const isDark = ref(false);
watchEffect(() => {
// For regular Vue apps
document.documentElement.classList.toggle('dark', isDark.value);
});
// For web components, the theme prop handles this
// <form-component theme="dark" />
</script>
<template>
<button @click="isDark = !isDark">
{{ isDark ? '☀️ Light' : '🌙 Dark' }}
</button>
</template>

The theme includes the full Tailwind color palette in OKLCH:

@theme {
/* Red */
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-500: oklch(0.637 0.237 25.331);
--color-red-900: oklch(0.396 0.141 25.723);
/* Green */
--color-green-50: oklch(0.982 0.018 155.826);
--color-green-500: oklch(0.723 0.219 149.579);
--color-green-900: oklch(0.393 0.095 152.535);
/* Blue */
--color-blue-50: oklch(0.97 0.014 254.604);
--color-blue-500: oklch(0.623 0.214 259.815);
--color-blue-900: oklch(0.379 0.146 265.522);
/* And all other Tailwind colors... */
}

  1. Use semantic colors (primary, danger, etc.) not raw colors
  2. Test in both modes - always verify light and dark themes
  3. Maintain contrast - use *-foreground colors for text on colored backgrounds
  4. Prefer OKLCH for custom colors - better perceptual uniformity