After updating iOS to dark theme, half the buttons become unreadable, and on Android the app crashes when switching themes? That's a familiar situation if colors are hardcoded. We implement dark mode with automatic switching, semantic tokens, and WCAG AA contrast. Our experience shows that proper implementation increases user retention by 20–30%, while mistakes in dark theme ruin the app experience.
How not to do it: inversion and hardcoded colors
The first and most costly mistake is using hardcoded colors instead of semantic tokens. Color.white, #FFFFFF, UIColor(red:1 green:1 blue:1 alpha:1) — all of these break when switching themes. Fixing later means rewriting all UI components.
The correct approach is semantic tokens: background.primary, text.secondary, surface.elevated, accent.default. On iOS that's UIColor.systemBackground, UIColor.label, UIColor.secondaryLabel and custom colors via Asset Catalog with Light and Dark variants. On Android — Material Design 3 with colorScheme through MaterialTheme.colorScheme.surface, onSurface, surfaceVariant. In Flutter: ThemeData.light() and ThemeData.dark() with a full set of ColorScheme, switching via MaterialApp(themeMode: ...). In React Native: useColorScheme() hook from core + Appearance.getColorScheme() for initialization.
Why semantic tokens are the only correct approach?
Semantic tokens allow you to change the theme centrally without editing each screen. Compare: with hardcode, changing the background color requires finding and replacing 50+ occurrences. With tokens, you change the value of one variable. That's 5 times faster and eliminates errors. According to Apple HIG, using system colors and semantic tokens is mandatory for passing review (Section 4.2).
| Approach | Time to change color | Error risk | Flexibility |
|---|---|---|---|
| Hardcode | 30–60 minutes (50+ edits) | High | Low |
| Semantic tokens | 2–5 minutes | Low | High |
What rules to follow for the dark palette?
Dark theme is not just a dark background. Here are the main rules that are most often violated.
Elevation through lightening, not shadows
In Material Design 3, surfaces at different z-index levels in dark theme differ by brightness: the higher, the lighter. surface → surfaceContainer → surfaceContainerHigh. Shadows are almost invisible in dark theme — they are replaced by tonal separation.
Text contrast
WCAG AA requires at least 4.5:1 for normal text. White #FFFFFF on dark #121212 = 18.1:1 — too high, straining eyes. Optimal #E0E0E0 on #121212 = 14.7:1. Google recommends #FFFFFF with 87% opacity for primary text.
| Text type | Minimum contrast (WCAG AA) | Target in dark theme |
|---|---|---|
| Normal text | 4.5:1 | 14:1 |
| Large text (>=18pt) | 3:1 | 7:1 |
| Accent elements | 3:1 | Adapted |
Accent color
Many accent colors in dark theme need slight desaturation and lightening. 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 a problem. Solution: SVG illustrations with transparent background and adaptive colors via currentColor.
Dynamic switching
iOS since version 13 supports traitCollectionDidChange — the system automatically notifies about theme change. SwiftUI redraws with @Environment(\.colorScheme). UIKit requires explicit override func traitCollectionDidChange. Android: AppCompatDelegate.setDefaultNightMode() for programmatic switching. DayNight theme in styles.xml. Activity restarts on theme change — need to save state via ViewModel or onSaveInstanceState.
Important edge case: the user changes the theme in system settings while the app is in the background. When brought to the foreground, the app must apply the new theme without noticeable flickering. On Android this is recreate() activity or configChanges: uiMode in manifest with manual handling.
Testing dark theme
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) — check for "pure" black, 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.
What's included in the work
- Audit of existing codebase (finding all hardcoded colors)
- Converting colors to semantic tokens
- Creating dark palette in design system (Figma Variables + code)
- Implementing dynamic theme switching
- Testing contrast and readability (WCAG AA)
- Documentation on token usage and support during release
Estimated timelines
| Application | Timeline |
|---|---|
| New, tokens from scratch | 2–3 days |
| Existing, needs color refactoring | 3–5 days |
| Complex, many custom components | 5–7 days |
The cost is calculated individually after project audit. If you want to implement dark mode with quality guarantee, contact us — we'll evaluate your project for free. Over 5 years of experience and 50+ implemented projects in mobile development. Order dark mode implementation and get a consultation.







