How to add dark mode and a theme toggle in React Native

By the LumioUI team · Updated

To add dark mode in React Native, read the system setting with useColorScheme(), keep an optional user override in a theme context, and take every colour from theme tokens instead of hard-coding them. A toggle then just sets the override. LumioUI's ThemeProvider and Theme Toggle give you all of this, with a sun that turns into a crescent moon.

Live demo of the LumioUI Theme Toggle component. It's interactive, so try it.

How dark mode works in React Native

  1. Follow the system first. useColorScheme() returns "light" or "dark" and updates when the phone switches, for example at sunset. Most people never touch your toggle, so this default matters most.
  2. Let people override it. Store "system" | "light" | "dark" in context, and persist it (AsyncStorage or MMKV) so it survives restarts.
  3. Use semantic tokens. Screens ask for background and text, not #fff. With NativeWind that's bg-background and text-foreground. One theme change then recolours everything.
  4. Status bar and splash. Flip the status-bar style with the scheme, and set userInterfaceStyle: "automatic" in your Expo config.
TSX theme.tsx
import * as React from "react";
import { useColorScheme } from "react-native";

type Scheme = "light" | "dark";
const ThemeCtx = React.createContext<{ scheme: Scheme; setOverride: (s: Scheme | "system") => void }>(null!);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const system = useColorScheme() ?? "light";          // follows the phone
  const [override, setOverride] = React.useState<Scheme | "system">("system");
  const scheme = override === "system" ? system : override;
  return <ThemeCtx.Provider value={{ scheme, setOverride }}>{children}</ThemeCtx.Provider>;
}

export const useTheme = () => React.useContext(ThemeCtx);

// Colours come from tokens, never hard-coded per screen:
export const colors = {
  light: { background: "#fafafa", text: "#0b0b0b" },
  dark: { background: "#0f0f0f", text: "#f2f2f2" },
};

The quick way: LumioUI theme + Theme Toggle

npx @lumioui/cli init installs the theme (tokens, ThemeProvider, the NativeWind setup) into lib/lumioui. Wrap your app once:

TSX _layout.tsx
// app/_layout.tsx (Expo Router) or App.tsx
import "./global.css";
import { ThemeProvider } from "@/lib/lumioui";

export default function RootLayout() {
  return <ThemeProvider>{/* your app */}</ThemeProvider>;
}

Drop in the toggle wherever people expect it:

TSX Header.tsx
import { ThemeToggle } from "@/components/ui/theme-toggle/theme-toggle";

// In a header or settings row. Without a scheme prop it reads and sets
// the app theme for you; variant: "icon" | "pill" | "switch".
<ThemeToggle variant="icon" />

Style screens with semantic classes, and they follow the theme with no extra code:

TSX Screen.tsx
<View className="bg-background">
  <Text className="text-foreground">Follows light and dark automatically</Text>
  <Text className="text-muted-foreground">So does this</Text>
</View>
  • useTheme() exposes scheme, colors (for things classes can't reach, like SVG strokes) and setSchemeOverride("system" | "light" | "dark").
  • Pass scheme and onSchemeChange to ThemeToggle to control it yourself. The callback also tells you where the tap happened.
  • Use that tap position with Circle Reveal to flood the new theme out from the button.

Theming goes further, with accents, custom tokens and fixed-scheme sections. See the theming docs.

Add it to your app

LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:

npx @lumioui/cli init
npx @lumioui/cli add theme-toggle

It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Theme Toggle API.

More React Native guides