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