skip to content

Dark Mode & Theming

useColorScheme reports the system light or dark scheme, PlatformColor reads native semantic colors, and a context theme swaps palettes. Interviewers ask how to theme without rebuilding every style.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In React Native, how does useColorScheme report the system light or dark setting, and why prefer it over Appearance.getColorScheme()?

level: juniorimportance: must knowfreq 68%

answer

  1. a hook versus a snapshot
  2. 'light', 'dark' or null
  3. re-renders on every scheme change
  4. sunset schedules and app overrides
  5. addChangeListener outside components

basics

~10 s

useColorScheme returns 'light' or 'dark' (null only without the native Appearance module) and re-renders the component whenever the scheme changes. Appearance.getColorScheme() returns the same value once, so a stored result goes stale.

solid answer

~40 s

`useColorScheme()` from `react-native` returns the **active** color scheme, `'light'` or `'dark'`, and `null` only when the native Appearance module is missing, for example on some out-of-tree platforms. It is built on `useSyncExternalStore` over `Appearance.addChangeListener`, so the component re-renders whenever the scheme changes: the user flips the system setting, a sunset schedule switches to dark, or the app calls `Appearance.setColorScheme()`. `Appearance.getColorScheme()` returns the same value but only at the moment you call it, so a value cached at module scope or in a ref goes stale. Use the hook in components, and `Appearance.addChangeListener`, which returns a subscription with `remove()`, in non-React code. In 0.87 the hook no longer returns `'unspecified'`; treat `null` as your default scheme.

code

tsx · 16 lines
tsx
import { StyleSheet, Text, View, useColorScheme } from 'react-native';

export function BalanceCard({ balance }: { balance: string }) {
  const scheme = useColorScheme();
  const dark = scheme === 'dark';
  return (
    <View style={[styles.card, { backgroundColor: dark ? '#1c1f24' : '#ffffff' }]}>
      <Text style={[styles.amount, { color: dark ? '#f2f4f7' : '#11151a' }]}>{balance}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 20, borderRadius: 12 },
  amount: { fontSize: 28, fontWeight: '700' },
});

go deeper

for a junior

Recall that useColorScheme returns 'light' or 'dark' and re-renders on change, while getColorScheme returns a one-off value.

for a middle

Explain how the hook subscribes through addChangeListener and useSyncExternalStore, which events trigger it, and what null means.

for a senior

Structure theming so one subscription feeds the whole app, and remove stale module-scope or state-captured scheme reads from a codebase.

for a principal

Decide whether the product follows the system only or offers an in-app choice, and how that choice interacts with scheduled system switches.

## Two ways to read the scheme React Native exposes the user's appearance preference through the `Appearance` module, modelled on the web's `prefers-color-scheme`. On Android it maps to the system Dark theme (Android 10 and later), and on iOS to Dark Mode (iOS 13 and later). | API | Kind | Updates the UI when the scheme changes | |---|---|---| | `useColorScheme()` | React hook | Yes, re-renders the component | | `Appearance.getColorScheme()` | Function | No, returns the value at call time | | `Appearance.addChangeListener(listener)` | Subscription | Calls the listener; you decide what to update | The docs recommend the hook for components, and warn that styles or logic depending on the scheme should not cache `getColorScheme()`, because the value can change while the app runs. ## Return values - `'light'`: the light scheme is active. - `'dark'`: the dark scheme is active. - `null`: only when the native Appearance module is unavailable, which in practice means some out-of-tree platforms. Since React Native 0.87 the hook's return type is `ColorSchemeName | null` and it **no longer returns `'unspecified'`**. Code written for older versions that checked for `'unspecified'` can drop that branch; a simple `scheme === 'dark'` treats `null` as light. ## How the hook stays current The implementation is two lines of substance: 1. A `subscribe` function calls `Appearance.addChangeListener(onStoreChange)` and returns a cleanup that calls `remove()` on the subscription. 2. The hook returns `useSyncExternalStore(subscribe, getColorScheme)`. `useSyncExternalStore` re-reads `getColorScheme()` whenever the listener fires and re-renders only if the value changed. The listener fires for three kinds of change: - the user switches the system setting, - a scheduled switch, such as dark at sunset and light at sunrise, - the app itself calls `Appearance.setColorScheme('light' | 'dark' | 'auto')`. ## A banking app that follows the system For a banking app that simply follows the phone, the root of the theme is one hook call: ```tsx const scheme = useColorScheme(); const palette = scheme === 'dark' ? darkPalette : lightPalette; ``` Everything else, the balance card, the transaction list, the transfer form, reads its colours from that palette, usually through a context so that only one component subscribes. When the user's phone switches to dark at sunset, the hook fires once, the palette changes, and the screens re-render with the new colours. ## Outside React components Some code needs the scheme but is not a component, such as a chart library configured imperatively or a function that builds a native notification's colours: - Read the current value with `Appearance.getColorScheme()` at the moment you need it. - Subscribe with `const sub = Appearance.addChangeListener(({ colorScheme }) => { ... })`. - Call `sub.remove()` when you no longer need updates. On iOS and Android the `colorScheme` passed to the listener is always `'light'` or `'dark'`. ## Common mistakes - **Module-scope reads**: `const isDark = Appearance.getColorScheme() === 'dark'` at the top of a file is evaluated once, at import. - **Stuffing the value into state**: `useState(Appearance.getColorScheme())` captures the initial value and never updates. - **Assuming null means dark or means an error**: it only means the native module is absent. - **Subscribing in every component**: many components calling the hook works, but a single provider at the root is simpler and keeps the colour logic in one place. - **Forgetting the native configuration**: an app config or native project that restricts the app to the light style makes the reported scheme stay light even when the system is dark. ## Testing scheme changes without touching the device Since React Native 0.86, React Native DevTools can **emulate light and dark mode** from its command palette, so you can flip the scheme while stepping through a screen without opening the device's settings. The emulation is temporary and resets when DevTools disconnects. It complements, rather than replaces, a real test on devices: - Flip the real system setting with the app open to exercise the native path. - Relaunch in dark mode to check the first frame. - Leave the app running across a scheduled switch if the design depends on it.

  • Why does useColorScheme use useSyncExternalStore instead of useState plus an effect?
    The scheme is external state owned by the native Appearance module. `useSyncExternalStore` subscribes before the value is read for commit, so a change between render and subscription cannot be missed, and every component reading it during one render sees the same value. A `useState` initialised from `getColorScheme()` needs extra code to catch changes that happen before its effect subscribes.
  • What should a component do when useColorScheme returns null?
    Treat it as the app's default scheme, usually light. `null` means only that the native Appearance module is unavailable, which on iOS and Android does not happen in practice. A check such as `scheme === 'dark'` handles it naturally, with no separate branch.

saying these in an interview costs you the question

  • Appearance.getColorScheme() re-renders the component when the scheme changes.
  • useColorScheme returns null when the user has not chosen a scheme.
  • Reading the scheme once at app start is enough, since it only changes on restart.
  • useColorScheme still returns 'unspecified' in React Native 0.87.
  • The hook only reacts to the system setting, not to Appearance.setColorScheme calls.
open as a page

In a React Native app, how do you theme colors through a context-provided palette while keeping layout in a static StyleSheet?

level: middleimportance: must knowfreq 58%

basics

~10 s

Keep layout (padding, flex, radii, fonts) in a module-scope StyleSheet.create, select a light or dark palette object from useColorScheme in one provider, share it through context, and apply colours per render in style arrays.

open as a page

In React Native, what does Appearance.setColorScheme override, and how does a banking app return to following the system on 0.87 versus 0.86?

level: middleimportance: should knowfreq 36%

basics

~20 s

Appearance.setColorScheme('light' | 'dark') forces the app, including its native alerts and pickers, into one scheme without touching the system setting. On 0.87 'auto' removes the override; on 0.86 (Expo SDK 57) the reset value is 'unspecified'.

open as a page

A React Native banking app keeps light colors after the phone switches to dark mode with the app open; what are the likely causes and fixes?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Either the reported scheme is wrong, from native config restricting the app to light or a leftover setColorScheme override, or it is right but not read reactively, from colours captured at module scope, in initial state or outside context.

open as a page

In React Native, what does PlatformColor return, and when would a banking app use it instead of its own hex palette?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

PlatformColor('name', ...fallbacks) returns an opaque reference to a native system colour, resolved on the native side. It suits UI that should match the platform, such as label and background colours, and it adapts to theme and contrast settings.

open as a page