Dark mode done properly: the checklist we run on every app
The checks we use for a dark theme that feels designed rather than inverted: surfaces, colour, contrast, assets, system bars, launch flashes, charts and testing.

For people who keep their phone in dark mode, your dark theme is not a variant: it is the product. A dark theme built by flipping colours usually shows it within seconds. Shadows vanish, brand colours glow, logos disappear into the background, and the app flashes white on launch.
This is the checklist we run when we design and review a dark theme for iOS, Android and the web. It leans on Apple's Human Interface Guidelines, Material Design 3 and WCAG, and notes where those sources disagree.
1. Start from tokens, not from the light theme
- Do not invert the light themeApple's guidance says it plainly: dark mode colours are not simply inversions of their light counterparts. Inverted greys produce muddy mid-tones, and inverted brand colours rarely match the brand.
- Define semantic tokens with one value per themeName colours by role (surface, surface-raised, text-primary, text-secondary, border, accent, danger), never by value. Components use roles only. On iOS that maps to Color Sets with light and dark variants in the asset catalog; on Android to the Material colour roles in a DayNight theme.
- Ban hard-coded colours in componentsA single white hex value in a list cell is the most common dark mode bug we find in reviews. Add a lint rule or a code review check for raw colour values outside the token files.
2. Surfaces and elevation
- Show depth with lighter surfaces, not only with shadowsShadows barely read on a dark background. Material 3 handles this with tonal surface colours: surface container roles that run from lowest to highest, where higher containers are lighter. Apple does something similar on iOS with two sets of dark background colours: a dimmer base set, and a brighter set for interfaces layered on top, such as sheets and popovers.
- Avoid pure black as the default background for most interfacesMaterial's baseline dark theme uses dark grey rather than black, which keeps shadows visible and reduces the harsh edge of light text on black. We default to a near-black grey.
- Know when black is rightOn OLED screens a black pixel is switched off, which can save power and gives deep contrast for media. Apple's own iOS base background in dark mode is black. For full-screen video, photo viewers and reading at night, true black can be the better choice. Make it a deliberate decision per surface, not a default.
3. Colour and contrast
- Desaturate and lighten brand colours for dark backgroundsStrongly saturated colours vibrate against dark grey and can look like they are glowing. Material's dark theme guidance recommends lighter, desaturated tones of the brand colour. Keep the hue; change lightness and chroma.
- Check contrast in both themes, for every pairWCAG 1.4.3 asks for 4.5:1 for body text and 3:1 for large text; 1.4.11 asks for 3:1 for icons, borders and focus rings. Apple's dark mode guidance sets 4.5:1 as the minimum and suggests aiming for 7:1 for small text on custom colours. Contrast is a property of the pair, so test secondary text on raised surfaces, not just on the page background.
- Do not go too far the other wayPure white text on pure black can make letters appear to bleed or glow for some readers, especially at small sizes. Slightly off-white text on a near-black surface keeps strong contrast without the glare.
- Test with Increase Contrast and Reduce TransparencyApple calls out that dark text on dark backgrounds can lose legibility when these settings are on, alone or together.
4. Images, illustrations and logos
- Provide dark variants where an asset fails on a dark backgroundApple's advice: use one asset if it works in both appearances, otherwise create separate light and dark versions. Line illustrations with dark strokes and logos with black wordmarks almost always need a variant.
- Tone down white-background imagesA product photo on a white square glows in a dark interface. Apple suggests slightly darkening such images; for e-commerce we prefer transparent cut-outs or a neutral tile.
- Use template icons that take a token colourSF Symbols, Material Symbols and single-colour SVGs tinted from a token adapt automatically. Multi-colour icons need checking one by one.
5. Follow the system, then decide on an override
- Follow the system setting by defaultPeople switch appearance in one place and expect every app to follow, including when the system changes automatically at sunset while your app is open.
- Think twice before adding an in-app appearance settingApple's guidelines recommend avoiding an app-specific appearance setting, because people then have to manage two switches and may think the app is broken. Many apps add one anyway. If you do, make "System" the default option and offer Light and Dark as explicit overrides.
- Persist the override and apply it before the first frameRead the stored choice synchronously at startup, or the app will render in the system theme and then switch.
6. System bars and the launch flash
- Set status bar and navigation bar icon colours per themeLight icons on dark bars, dark icons on light bars. On Android use WindowInsetsControllerCompat (setAppearanceLightStatusBars and setAppearanceLightNavigationBars); in React Native, the StatusBar barStyle; on iOS, let the status bar style follow the view controller's appearance.
- Make the launch screen theme-awareOn iOS, use asset catalog colours with dark variants in the launch screen. On Android, set the splash background and window background from a DayNight theme so the first frame is already dark.
- Give web views and loading states a dark backgroundA WebView, an image placeholder or a skeleton loader with a white background flashes during load. Set the container background from your surface token, and serve web content with a dark theme too.
- On the web, declare color-schemeThe color-scheme CSS property, or the matching meta tag in the head, lets the browser paint the right default background, form controls and scrollbars before your stylesheet loads.
7. Charts and data
- Give charts their own dark paletteSeries colours tuned for white often lose separation on dark grey. Re-check every series colour against the dark surface for 3:1 non-text contrast, and against each other.
- Lighten gridlines and axes deliberatelyGridlines should recede, not disappear. A low-contrast token for gridlines and a stronger one for axis labels works in both themes.
- Never rely on colour alone in chartsWCAG 1.4.1 applies in both themes: direct labels, patterns or markers keep series distinguishable.
8. Testing
- Review every screen in both themes before releaseInclude empty, error, loading and offline states, sheets and dialogs, keyboard and pickers, and emails or web pages the app opens.
- Switch theme while the app is runningOn Android a theme change is a configuration change that recreates activities by default; on iOS trait changes update views live. Check that state, scroll position and form input survive.
- Capture screenshots for both themes in UI testsSnapshot tests that run in light and dark catch the hard-coded white cell before a user does.
- Check on a real OLED device at low brightnessSimulators hide smearing on black, glowing whites and colours that are too saturated.
Code: the minimum setup
On the web, two lines do most of the work: declare that the page supports both schemes, then swap token values in a prefers-color-scheme media query.
:root {
color-scheme: light dark; /* native controls and scrollbars follow */
--surface: #ffffff;
--surface-raised: #f2f2f5;
--text: #1b1b1f;
--accent: #4b3fd8;
}
@media (prefers-color-scheme: dark) {
:root {
--surface: #141418;
--surface-raised: #202027; /* lighter, not a shadow */
--text: #e6e6ea;
--accent: #a9a1ff; /* lighter, less saturated */
}
}
body { background: var(--surface); color: var(--text); }
In React Native, the useColorScheme hook returns the current system scheme and re-renders when it changes. It can return null when the native Appearance module is not available, so treat anything other than "dark" as light.
import { useColorScheme } from 'react-native';
import { lightTokens, darkTokens } from './tokens';
export function useTokens() {
const scheme = useColorScheme(); // 'light' | 'dark' | null
return scheme === 'dark' ? darkTokens : lightTokens;
}
Where we land
A good dark theme is mostly a token problem. If every colour in the product has a role and two values, dark mode becomes a review of pairs rather than a rewrite of screens. Our order of work is tokens, then surfaces and contrast, then assets, then the launch and system-bar details that make the theme feel finished.
Sources
- Dark Mode, Human Interface Guidelines · Apple
- Color roles · Material Design 3
- Dark theme (Material Components for Android) · Google, Material Components
- Implement dark theme · Android Developers
- prefers-color-scheme: Hello darkness, my old friend · web.dev
- color-scheme CSS property · MDN
- Understanding Success Criterion 1.4.3: Contrast (Minimum) · W3C WAI
- useColorScheme · React Native


