Theming System

Matrix UI provides a powerful theming system based on CSS custom properties, enabling complete visual customization while maintaining consistency.

Built-in Themes

Matrix UI comes with 5 professionally designed themes:

Theme Structure

Each theme defines a set of design tokens using CSS custom properties:

// Theme structure with light and dark mode support
interface Theme {
  name: string
  colors: ThemeColors      // Light mode colors
  darkColors: ThemeColors  // Dark mode colors
  gradients: {
    primary: string
    secondary: string
    accent: string
  }
}

// Color tokens (HSL format: "hue saturation% lightness%")
interface ThemeColors {
  background: string
  foreground: string
  card: string
  'card-foreground': string
  popover: string
  'popover-foreground': string
  primary: string
  'primary-foreground': string
  secondary: string
  'secondary-foreground': string
  muted: string
  'muted-foreground': string
  accent: string
  'accent-foreground': string
  destructive: string
  'destructive-foreground': string
  success: string
  'success-foreground': string
  warning: string
  'warning-foreground': string
  border: string
  input: string
  ring: string
  radius: string
  // Chart colors: 'chart-1' through 'chart-8'
}

Design Tokens

Matrix UI uses a three-tier token system:

Raw Tokens

Base color values

--matrix-blue-500: #3b82f6
--matrix-gray-900: #111827
--matrix-red-600: #dc2626

Semantic Tokens

Contextual meanings

--primary: var(--matrix-blue-500)
--background: var(--matrix-white)
--destructive: var(--matrix-red-600)

Component Tokens

Component-specific values

--button-bg: var(--primary)
--button-hover: var(--primary-hover)
--card-border: var(--border)

Applying Themes

Using the Theme Utils

import { applyTheme, getStoredTheme, getStoredMode, type ColorMode } from '@matrix-ui/themes'

// Apply a theme with light mode (default)
applyTheme('carbon')

// Apply a theme with dark mode
applyTheme('obsidian', 'dark')

// Restore user's saved preferences
const savedTheme = getStoredTheme() || 'carbon'
const savedMode = getStoredMode()  // Returns 'light' if not set
applyTheme(savedTheme, savedMode)

// Toggle between light and dark mode
function toggleDarkMode() {
  const theme = getStoredTheme() || 'carbon'
  const currentMode = getStoredMode()
  const newMode: ColorMode = currentMode === 'light' ? 'dark' : 'light'
  applyTheme(theme, newMode)
}

In a Next.js App

// app/layout.tsx
import type { Metadata } from 'next'

// Flash prevention script - runs before React hydration
const themeScript = `
(function() {
  try {
    var mode = localStorage.getItem('matrix-mode');
    if (mode === 'dark') {
      document.documentElement.classList.add('dark');
    }
  } catch (e) {}
})();
`

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script dangerouslySetInnerHTML={{ __html: themeScript }} />
      </head>
      <body>{children}</body>
    </html>
  )
}

// In a client component:
'use client'
import { useEffect } from 'react'
import { applyTheme, getStoredTheme, getStoredMode } from '@matrix-ui/themes'

export function ThemeProvider({ children }) {
  useEffect(() => {
    const theme = getStoredTheme() || 'carbon'
    const mode = getStoredMode()
    applyTheme(theme, mode)
  }, [])

  return children
}

Creating Custom Themes

You can create your own themes by defining both light and dark color sets:

// Define a custom theme with light and dark modes
const myCustomTheme = {
  name: 'my-theme',
  colors: {
    // Light mode colors (HSL format)
    background: '0 0% 100%',
    foreground: '240 10% 3.9%',
    card: '0 0% 100%',
    'card-foreground': '240 10% 3.9%',
    primary: '346.8 77.2% 49.8%',
    'primary-foreground': '355.7 100% 97.3%',
    // ... all 32 color tokens
  },
  darkColors: {
    // Dark mode colors
    background: '20 14.3% 4.1%',
    foreground: '0 0% 95%',
    card: '24 9.8% 8%',
    'card-foreground': '0 0% 95%',
    primary: '346.8 77.2% 49.8%',
    'primary-foreground': '355.7 100% 97.3%',
    // ... all 32 color tokens
  },
  gradients: {
    primary: 'from-rose-500 to-rose-600',
    secondary: 'from-gray-100 to-gray-200',
    accent: 'from-rose-50 to-rose-100',
  }
}

// Register and use custom theme (coming soon)
// For now, override CSS variables directly in your globals.css

Theme Generator

Use the CLI to generate a theme CSS file:

# Generate theme CSS with both light and dark modes
npx matrix-ui theme generate --name carbon --mode both --out ./matrix-theme.css

# Generate only dark mode
npx matrix-ui theme generate --name obsidian --mode dark

Dark Mode

Matrix UI has built-in dark mode support for all themes:

Using Built-in Dark Mode

import { applyTheme, getStoredTheme, getStoredMode, type ColorMode } from '@matrix-ui/themes'

// Apply dark mode
applyTheme('carbon', 'dark')

// Toggle between modes
function toggleDarkMode() {
  const theme = getStoredTheme() || 'carbon'
  const currentMode = getStoredMode()
  const newMode: ColorMode = currentMode === 'light' ? 'dark' : 'light'
  applyTheme(theme, newMode)
}

// The applyTheme function automatically:
// 1. Sets CSS variables for the selected mode
// 2. Toggles the 'dark' class on <html>
// 3. Saves preference to localStorage

Listening to Theme Changes

// Listen for theme and mode changes
window.addEventListener('theme-change', (event) => {
  const { theme, mode } = event.detail
  console.log(`Theme: ${theme}, Mode: ${mode}`)
})

System Preference Detection

import { applyTheme, getStoredTheme, getStoredMode, type ColorMode } from '@matrix-ui/themes'

// Detect and apply system preference on first visit
function initializeTheme() {
  const storedMode = localStorage.getItem('matrix-mode')

  // If user hasn't set a preference, use system preference
  if (!storedMode) {
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
    const mode: ColorMode = prefersDark ? 'dark' : 'light'
    applyTheme('carbon', mode)
  } else {
    const theme = getStoredTheme() || 'carbon'
    const mode = getStoredMode()
    applyTheme(theme, mode)
  }
}

// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    const theme = getStoredTheme() || 'carbon'
    const mode: ColorMode = e.matches ? 'dark' : 'light'
    applyTheme(theme, mode)
  })

Theme Customization

Overriding Specific Tokens

/* In your global CSS */
:root {
  /* Override primary color */
  --primary: 262.1 83.3% 57.8%;
  
  /* Override radius */
  --radius: 0.75rem;
  
  /* Override specific component */
  --button-height: 2.75rem;
}

Component-Level Theming

// Using className for one-off customization
<Button 
  className="bg-purple-600 hover:bg-purple-700 text-white"
>
  Custom Styled Button
</Button>

// Creating variant with CVA
const customButtonVariants = cva(
  "inline-flex items-center justify-center",
  {
    variants: {
      variant: {
        brand: "bg-brand text-brand-foreground hover:bg-brand/90",
      }
    }
  }
)

Best Practices

Use Semantic Tokens

Always use semantic tokens (e.g., --primary) instead of raw colors to ensure your UI adapts to theme changes.

Maintain Contrast Ratios

Ensure your custom themes meet WCAG accessibility standards with proper contrast ratios between foreground and background colors.

Test Across Themes

Always test your components with multiple themes to ensure they look good in both light and dark modes.

Color Format

Matrix UI uses HSL color format for better manipulation and consistency:

HSL Format:
222.2 47.4% 11.2%
CSS Usage:
hsl(var(--primary))
With Opacity:
hsl(var(--primary) / 0.8)