Skip to content

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

TokenLightDarkNotes
bg-body#FAFAF7#1C1A17Warm near-black, not cold #000
bg-card#FFFFFF#262420Elevated from body
bg-inset#F3F1EC#2E2C28Recessed surfaces
bg-hover#EDEAE3#363330Hover/active state

Border Colors

TokenLightDark
border#E5E2DB#3D3A35
border-strong#D4D0C8#4A4640

Text Colors

TokenLightDarkContrast on Dark bg-body
text-1#1A1816#F5F3EF14.2:1
text-2#57534E#B5B0A87.8:1
text-3#A8A29E#6E6A643.4:1

Brand & Semantic Colors

TokenLightDarkNotes
Brand Green#2D6B4A#4A9E72Brightened for dark bg contrast
Success#2D7D46#4A9E72Same as brand dark
Warning#D4910A#E8A820Slightly brighter
Danger#C4463A#E05A4EBrighter red
Info#2563EB#4A8AFFLighter blue

Soft Backgrounds (Badges, Alerts)

TokenLightDark
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:

LevelLightDark
shadow-smrgba(28,24,20, 0.06)rgba(0,0,0, 0.3)
shadow-mdrgba(28,24,20, 0.06)rgba(0,0,0, 0.3)
shadow-lgrgba(28,24,20, 0.06)rgba(0,0,0, 0.3)
shadow-floatrgba(28,24,20, 0.12)rgba(0,0,0, 0.4)

Web Implementation

HTML Attribute

Use data-theme on the <html> element:

html
<html data-theme="dark">

Toggle Logic

ts
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:

ts
// tailwind.config.ts
export default {
  darkMode: ['selector', '[data-theme="dark"]'],
  // ...
}

Mobile Implementation (React Native)

useColorScheme Hook

ts
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:

tsx
<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:

CheckDescription
[ ] Surface hierarchybg-body < bg-card < bg-inset visible as layers
[ ] Text contrastAll text meets WCAG AA (4.5:1 for body, 3:1 for large)
[ ] Brand greenButtons, links, active nav use #4A9E72 not #2D6B4A
[ ] Borders visibleCard borders visible but subtle on dark surfaces
[ ] Badge legibilityStatus badges readable on dark card backgrounds
[ ] Input fieldsClear boundary between input and background
[ ] Focus ringsFocus indicators visible (green ring on dark bg)
[ ] ShadowsShadows deeper but not causing harsh edges
[ ] Charts/graphsData visualization colors distinguishable
[ ] Empty statesIllustrations have dark-friendly variants
[ ] Modals/sheetsBackdrop dim and modal surface distinct
[ ] Toast notificationsReadable on both surfaces
[ ] Skeleton loadersPulse animation visible
[ ] ScrollbarCustom scrollbar colors match dark theme

Common mistakes

  • Using #000000 as 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 opacity for 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.

RetailOS - Sistem ERP Retail Modern untuk Indonesia