Dark Mode
Complete dark mode implementation guide including token mapping, implementation strategy, and testing checklist.
Token Mapping
Every light mode token has a dark mode equivalent. The dark palette maintains warm undertones consistent with the Bumi aesthetic.
Surface Colors
| Token | Light | Dark | Notes |
|---|---|---|---|
bg-body | #FAFAF7 | #1C1A17 | Warm near-black, not cold #000 |
bg-card | #FFFFFF | #262420 | Elevated from body |
bg-inset | #F3F1EC | #2E2C28 | Recessed surfaces |
bg-hover | #EDEAE3 | #363330 | Hover/active state |
Border Colors
| Token | Light | Dark |
|---|---|---|
border | #E5E2DB | #3D3A35 |
border-strong | #D4D0C8 | #4A4640 |
Text Colors
| Token | Light | Dark | Contrast on Dark bg-body |
|---|---|---|---|
text-1 | #1A1816 | #F5F3EF | 14.2:1 |
text-2 | #57534E | #B5B0A8 | 7.8:1 |
text-3 | #A8A29E | #6E6A64 | 3.4:1 |
Brand & Semantic Colors
| Token | Light | Dark | Notes |
|---|---|---|---|
| Brand Green | #2D6B4A | #4A9E72 | Brightened for dark bg contrast |
| Success | #2D7D46 | #4A9E72 | Same as brand dark |
| Warning | #D4910A | #E8A820 | Slightly brighter |
| Danger | #C4463A | #E05A4E | Brighter red |
| Info | #2563EB | #4A8AFF | Lighter blue |
Soft Backgrounds (Badges, Alerts)
| Token | Light | Dark |
|---|---|---|
success-soft | #E8F0EC | #1A3328 |
warning-soft | #FEF3C7 | #3D2E0A |
danger-soft | #FCEAE8 | #3D1F1C |
info-soft | #DBEAFE | #1A2744 |
Shadows
Dark mode shadows use deeper opacity since surfaces are already dark:
| Level | Light | Dark |
|---|---|---|
shadow-sm | rgba(28,24,20, 0.06) | rgba(0,0,0, 0.3) |
shadow-md | rgba(28,24,20, 0.06) | rgba(0,0,0, 0.3) |
shadow-lg | rgba(28,24,20, 0.06) | rgba(0,0,0, 0.3) |
shadow-float | rgba(28,24,20, 0.12) | rgba(0,0,0, 0.4) |
Web Implementation
HTML Attribute
Use data-theme on the <html> element:
<html data-theme="dark">Toggle Logic
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme')
const next = current === 'dark' ? 'light' : 'dark'
document.documentElement.setAttribute('data-theme', next)
localStorage.setItem('theme', next)
}
// On load: respect system preference, then localStorage override
function initTheme() {
const stored = localStorage.getItem('theme')
if (stored) {
document.documentElement.setAttribute('data-theme', stored)
return
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark')
}
}CSS Structure
All dark tokens are defined under [data-theme='dark'] selector (see Web CSS Tokens for the full file).
Tailwind Dark Mode
Configure Tailwind to use the attribute strategy:
// tailwind.config.ts
export default {
darkMode: ['selector', '[data-theme="dark"]'],
// ...
}Mobile Implementation (React Native)
useColorScheme Hook
import { useColorScheme } from 'react-native'
export function useTheme() {
const systemScheme = useColorScheme()
// Allow user override stored in AsyncStorage
const [override, setOverride] = useState<'light' | 'dark' | 'system'>('system')
const scheme = override === 'system' ? systemScheme : override
const colors = scheme === 'dark' ? darkColors : lightColors
return { scheme, colors, setOverride }
}NativeWind Dark Mode
NativeWind supports the dark: prefix automatically when using useColorScheme:
<View className="bg-surface-body dark:bg-[#1C1A17]">
<Text className="text-text-1 dark:text-[#F5F3EF]">Hello</Text>
</View>Images & Illustrations
- Isometric illustrations: provide separate light/dark variants
- If only one variant exists, add a subtle overlay in dark mode
- Logos: use light version (white/light text) in dark mode
- Photos: add 5% brightness reduction overlay in dark mode
Testing Checklist
Use this checklist when verifying dark mode for any screen:
| Check | Description |
|---|---|
| [ ] Surface hierarchy | bg-body < bg-card < bg-inset visible as layers |
| [ ] Text contrast | All text meets WCAG AA (4.5:1 for body, 3:1 for large) |
| [ ] Brand green | Buttons, links, active nav use #4A9E72 not #2D6B4A |
| [ ] Borders visible | Card borders visible but subtle on dark surfaces |
| [ ] Badge legibility | Status badges readable on dark card backgrounds |
| [ ] Input fields | Clear boundary between input and background |
| [ ] Focus rings | Focus indicators visible (green ring on dark bg) |
| [ ] Shadows | Shadows deeper but not causing harsh edges |
| [ ] Charts/graphs | Data visualization colors distinguishable |
| [ ] Empty states | Illustrations have dark-friendly variants |
| [ ] Modals/sheets | Backdrop dim and modal surface distinct |
| [ ] Toast notifications | Readable on both surfaces |
| [ ] Skeleton loaders | Pulse animation visible |
| [ ] Scrollbar | Custom scrollbar colors match dark theme |
Common mistakes
- Using
#000000as dark background (too cold, use#1C1A17) - Keeping light-mode brand green
#2D6B4A(too dark on dark bg, use#4A9E72) - Forgetting to update soft badge backgrounds (light colors on dark bg look wrong)
- Using
opacityfor dark mode instead of separate color tokens (causes muddy colors)
System preference
Always respect prefers-color-scheme as the default, but allow explicit user override that persists across sessions.