AI Skill Library

Dark Mode & Design Tokens

CSS custom properties, color systems, theme switching, system preference detection.

cssdesign-systemdark-modefrontend
# Dark Mode & Design Tokens

## CSS custom properties token system
```css
/* Design tokens -- single source of truth */
:root {
  /* Primitive tokens */
  --blue-500: #3b82f6;
  --gray-900: #111827;
  --gray-50:  #f9fafb;

  /* Semantic tokens (reference primitives) */
  --color-background:       var(--gray-50);
  --color-surface:          #ffffff;
  --color-text-primary:     var(--gray-900);
  --color-text-secondary:   #6b7280;
  --color-border:           #e5e7eb;
  --color-brand:            var(--blue-500);
  --color-brand-hover:      #2563eb;

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;

  /* Typography */
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --leading-tight: 1.25;

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
  --shadow-lg: 0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05);

  /* Radius */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-full: 9999px;
}

.dark {
  --color-background:       var(--gray-900);
  --color-surface:          #1f2937;
  --color-text-primary:     #f9fafb;
  --color-text-secondary:   #9ca3af;
  --color-border:           #374151;
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.4);
}
```

## Theme switching (React)
```tsx
type Theme = 'light' | 'dark' | 'system'

function useTheme() {
  const [theme, setTheme] = useState<Theme>(
    () => (localStorage.getItem('theme') as Theme) ?? 'system'
  )

  useEffect(() => {
    const root = document.documentElement
    const isDark = theme === 'dark' ||
      (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
    root.classList.toggle('dark', isDark)
    localStorage.setItem('theme', theme)
  }, [theme])

  // Listen for system change
  useEffect(() => {
    if (theme !== 'system') return
    const mq = window.matchMedia('(prefers-color-scheme: dark)')
    const handler = () => document.documentElement.classList.toggle('dark', mq.matches)
    mq.addEventListener('change', handler)
    return () => mq.removeEventListener('change', handler)
  }, [theme])

  return { theme, setTheme }
}
```

## Prevent flash of wrong theme (Next.js)
```tsx
// In <head> -- runs before paint
<script dangerouslySetInnerHTML={{ __html: `
  (function() {
    const t = localStorage.getItem('theme')
    const dark = t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)
    if (dark) document.documentElement.classList.add('dark')
  })()
`}} />
```

## Multi-theme with data attribute
```css
[data-theme='ocean'] {
  --color-brand: #06b6d4;
  --color-background: #ecfeff;
}
[data-theme='forest'] {
  --color-brand: #16a34a;
  --color-background: #f0fdf4;
}
[data-theme='sunset'] {
  --color-brand: #f97316;
  --color-background: #fff7ed;
}
```
```ts
document.documentElement.dataset.theme = 'ocean'
```

## Color system: accessible contrast
```ts
// Check contrast ratio (WCAG AA = 4.5:1 for normal text)
function getLuminance(r: number, g: number, b: number) {
  const [rs, gs, bs] = [r, g, b].map(c => {
    c /= 255
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
  })
  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
}
function contrastRatio(l1: number, l2: number) {
  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)
}
```

API: /api/skills/dark-mode-theming