Color Modes

Learn how to implement and customize dark mode and other color schemes in Aki UI.

Overview

Aki UI provides built-in support for multiple color modes, including light and dark themes. The components automatically adapt to the current color mode using CSS custom properties and Tailwind's dark mode utilities.

  • Automatic Detection: Respects user's system preference
  • Manual Toggle: Programmatic theme switching
  • Persistent Storage: Remembers user's choice
  • SSR Compatible: No flash of unstyled content

Setup

1. Configure Tailwind CSS

Enable dark mode in your Tailwind configuration:

// tailwind.config.js
module.exports = {
  darkMode: 'class', // Enable class-based dark mode
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    './node_modules/@akitectio/aki-ui/**/*.{js,ts,jsx,tsx}',
  ],
  // ... rest of your config
}

2. Theme Provider (Optional)

Create a theme provider to manage color mode state:

'use client'

import { createContext, useContext, useEffect, useState } from 'react'

type Theme = 'light' | 'dark' | 'system'

interface ThemeContextType {
  theme: Theme
  setTheme: (theme: Theme) => void
  actualTheme: 'light' | 'dark'
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('system')
  const [actualTheme, setActualTheme] = useState<'light' | 'dark'>('light')

  useEffect(() => {
    const savedTheme = localStorage.getItem('theme') as Theme
    if (savedTheme) {
      setTheme(savedTheme)
    }
  }, [])

  useEffect(() => {
    const updateTheme = () => {
      const isDark = theme === 'dark' || 
        (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
      
      setActualTheme(isDark ? 'dark' : 'light')
      document.documentElement.classList.toggle('dark', isDark)
    }

    updateTheme()
    
    if (theme !== 'system') {
      localStorage.setItem('theme', theme)
    }

    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    if (theme === 'system') {
      mediaQuery.addEventListener('change', updateTheme)
      return () => mediaQuery.removeEventListener('change', updateTheme)
    }
  }, [theme])

  return (
    <ThemeContext.Provider value={{ theme, setTheme, actualTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider')
  }
  return context
}

Theme Toggle Component

Create a theme toggle component for users to switch between modes:

'use client'

import { Button } from '@akitectio/aki-ui'
import { useTheme } from './theme-provider'

export function ThemeToggle() {
  const { theme, setTheme, actualTheme } = useTheme()

  const toggleTheme = () => {
    if (theme === 'light') {
      setTheme('dark')
    } else if (theme === 'dark') {
      setTheme('system')
    } else {
      setTheme('light')
    }
  }

  const getIcon = () => {
    if (theme === 'light') return '☀️'
    if (theme === 'dark') return '🌙'
    return '🖥️'
  }

  const getLabel = () => {
    if (theme === 'light') return 'Light'
    if (theme === 'dark') return 'Dark'
    return 'System'
  }

  return (
    <Button
      variant="outline"
      size="sm"
      onClick={toggleTheme}
      className="gap-2"
    >
      <span>{getIcon()}</span>
      <span>{getLabel()}</span>
    </Button>
  )
}

Component Implementation

Using Dark Mode Classes

Aki UI components use Tailwind's dark mode utilities. You can extend this pattern in your own components:

// Example component with dark mode support
function MyCard({ children }: { children: React.ReactNode }) {
  return (
    <div className="
      bg-white dark:bg-gray-800
      text-gray-900 dark:text-white
      border border-gray-200 dark:border-gray-700
      shadow-sm dark:shadow-gray-900/20
      rounded-lg p-6
    ">
      {children}
    </div>
  )
}

CSS Custom Properties

For more complex theming, use CSS custom properties:

:root {
  --color-background: 255 255 255;
  --color-foreground: 0 0 0;
  --color-primary: 59 130 246;
  --color-border: 229 231 235;
}

.dark {
  --color-background: 17 24 39;
  --color-foreground: 255 255 255;
  --color-primary: 96 165 250;
  --color-border: 55 65 81;
}

.my-component {
  background-color: rgb(var(--color-background));
  color: rgb(var(--color-foreground));
  border-color: rgb(var(--color-border));
}

Color Palette

Light Mode

Background
#FFFFFF
Surface
#F9FAFB
Primary
#3B82F6
Text
#111827

Dark Mode

Background
#111827
Surface
#1F2937
Primary
#60A5FA
Text
#F9FAFB

Best Practices

Design Guidelines

  • Contrast: Ensure sufficient contrast in both light and dark modes (WCAG AA: 4.5:1)
  • Consistency: Maintain visual hierarchy across both themes
  • Colors: Don't rely solely on color to convey information
  • Testing: Test all interactions in both color modes
  • Images: Consider different images or filters for dark mode

Implementation Tips

  • • Use semantic color names instead of specific values
  • • Prefer CSS custom properties for dynamic theming
  • • Test with system preference changes
  • • Consider reduced motion preferences
  • • Provide visual feedback for theme changes

Next.js App Router Setup

For Next.js App Router applications, wrap your app with the theme provider:

// app/layout.tsx
import { ThemeProvider } from '@/components/theme-provider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}