Dark Mode Development for Mobile Applications
Dark mode is not "make everything dark." It's a parallel color system that must provide the same contrast ratios, hierarchy, and readability as the light theme. If you simply invert colors, you get something visually incorrect: shadows become lighter than the background, accent color loses saturation, images will stand out from context.
What Not to Do: Inversion and Hardcoded Colors
The first and most expensive mistake is using hardcoded colors instead of semantic tokens. Color.white, #FFFFFF, UIColor(red:1 green:1 blue:1 alpha:1) — all this breaks when switching themes. Fixing this later in a finished app means rewriting all UI components.
The correct approach is semantic tokens: background.primary, text.secondary, surface.elevated, accent.default. On iOS this is UIColor.systemBackground, UIColor.label, UIColor.secondaryLabel and custom colors via Asset Catalog with Light and Dark variants. On Android — Material Design 3 with colorScheme via MaterialTheme.colorScheme.surface, onSurface, surfaceVariant.
In Flutter: ThemeData.light() and ThemeData.dark() with full ColorScheme, switching via MaterialApp(themeMode: ...). In React Native: useColorScheme() hook from core plus Appearance.getColorScheme() for initialization.
Rules for Dark Palette
Dark theme is not just a dark background. Several rules that are broken most often:
Elevation through lightening, not shadows. In Material Design 3, surfaces at different z-index levels in dark theme differ by brightness: higher, lighter. surface → surfaceContainer → surfaceContainerHigh. Shadows in dark theme are almost invisible — they are replaced by tonal separation.
Text contrast. WCAG AA requires minimum 4.5:1 for regular text. White #FFFFFF on dark #121212 = 18.1:1 — too high, fatigues eyes. Optimal is #E0E0E0 on #121212 = 14.7:1. Google recommends #FFFFFF with 87% opacity for primary text.
Accent color. Many accent colors in dark theme need to be slightly desaturated and lightened. Bright blue #2196F3 on dark background vibrates and causes discomfort. #90CAF9 is the correct version for dark mode.
Images and illustrations. Photos don't change. Illustrations with white background are problematic. Solution: SVG illustrations with transparent background and adaptive colors via currentColor.
Dynamic Switching
iOS from iOS 13 supports traitCollectionDidChange — the system automatically reports theme changes. SwiftUI redraws with @Environment(\.colorScheme). UIKit requires explicit override func traitCollectionDidChange.
Android: AppCompatDelegate.setDefaultNightMode() for programmatic switching. DayNight theme in styles.xml. Activity restarts when theme changes — need to save state via ViewModel or onSaveInstanceState.
Important edge case: user changes theme in system settings while app is running in background. When returning to foreground, app should apply new theme without noticeable flicker. On Android this is recreate() activity or configChanges: uiMode in manifest with manual handling.
Dark Theme Testing
Three levels:
- Figma — all components with light/dark variants via Figma Variables
- Simulator/emulator — switching via quick settings
- Physical device in OLED mode (iPhone 12+, Samsung Galaxy) — verify "black" purity, absence of halo effect around light elements
Tools: Xcode Accessibility Inspector for contrast checking, Android Accessibility Scanner. Check all screens, all modals, all alerts — they often use system colors and break first.
Process and Timeline
Audit existing codebase (find all hardcoded colors) → refactor to tokens → create dark palette in design system → implementation → testing.
| Application | Timeline |
|---|---|
| New, tokens from scratch | 2–3 days |
| Existing, needs color refactor | 3–5 days |
| Complex, many custom components | 5–7 days |
Cost is calculated individually after project audit.







