Dark Mode and Dynamic Theming Done Properly

The fastest way to ship a broken dark mode is inverting hardcoded hex colors screen by screen, which is roughly what we found when we inherited a project where a previous team had added dark mode as a late add-on. Text on cards was unreadable in about 15% of screens because shadow and elevation cues designed for a white background produced almost no contrast against dark gray surfaces, and semantic colors like 'error red' and 'success green' hadn't been re-tuned for contrast against a dark background.
We rebuilt theming around a semantic token system instead — colors named by role, like surface, surfaceElevated, textPrimary, textSecondary, accentPrimary — defined once per theme and referenced everywhere in code. No component references a raw hex value directly; every color reference goes through the token layer, which made adding a third theme, a high-contrast accessibility mode requested later by the client, a config change rather than a re-engineering effort.
Elevation in dark mode needed its own logic, not a straight port from light mode's drop-shadow approach. Material Design's guidance of lightening surface color slightly with each elevation level, rather than relying on shadows that barely read against dark backgrounds, matched what user testing preferred, so we adopted the same elevation-via-tint approach on iOS too, even though it's not the platform's native convention.
System-driven theme switching, respecting iOS's userInterfaceStyle and Android's day/night config qualifier and updating live when the OS theme changes without an app relaunch, was table stakes for us, but we also kept a manual override in-app settings, since a meaningful share of users we saw in analytics kept their phone in light mode system-wide but preferred dark mode specifically inside this one app.
Testing dark mode requires design review in both themes for every new screen, not just light mode with an assumption that dark 'should just work' from the token system. We added a dark-mode screenshot pass to our design review checklist after catching a new feature that looked fine in light mode but had a completely invisible disabled-button state in dark mode.

