ThemeSelector

System, Light or Dark — read from the live theme, and applied to it.

Open Source

Installation

ThemeSelector ships with the library — install it once, then import:

import { ThemeSelector } from "@delacour/react-native-ui/theme-selector";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add theme-selector

Usage

app/index.tsx
import { ThemeSelector } from "@/components/ui/theme-selector";
import { View } from "react-native";

export default function Example() {
  return (
    <View className="flex-1 justify-center p-4">
      <ThemeSelector />
    </View>
  );
}

How it reads the theme

The selection is Uniwind's, not the component's: System is selected while the app follows the device, so a theme changed anywhere else re-selects every mounted selector.

Basic
Basic
<ThemeSelector onValueChange={(mode) => console.log(mode)} />

Controlled

value with onValueChange holds the choice in your state, and applyTheme={false} keeps a press from re-theming the app.

Controlled
const [mode, setMode] = useState<ThemeMode>("system");

<ThemeSelector applyTheme={false} onValueChange={setMode} value={mode} />

Cards

variant="cards" stands the options apart as tiles, a larger glyph over each label.

Cards
Cards
<ThemeSelector variant="cards" />

Sizes

The button's label steps; sm reaches past its edges to stay a 44pt target.

Sizes
Sizes
<ThemeSelector size="sm" />
<ThemeSelector size="md" />
<ThemeSelector size="lg" />

Modes and labels

modes picks the options and their order, and labels renames them for localisation.

<ThemeSelector labels={{ light: "Day", dark: "Night" }} modes={["light", "dark"]} />

Persisting the choice

Uniwind forgets its theme on restart; with applyTheme={false} the selector only reports, so the app saves the choice, applies it and passes the saved mode back as value.

Persisted
Persisted
import { restoreThemeMode, ThemeSelector, type ThemeMode } from "@/components/ui/theme-selector";
import { useState } from "react";
import { createMMKV } from "react-native-mmkv";
import { Uniwind } from "uniwind";

const storage = createMMKV();

Uniwind.setTheme(restoreThemeMode(storage.getString("theme")));

export function AppearanceSetting() {
  const [saved, setSaved] = useState<ThemeMode>(() => restoreThemeMode(storage.getString("theme")));

  return (
    <ThemeSelector
      applyTheme={false}
      onValueChange={(mode) => {
        storage.set("theme", mode);
        setSaved(mode);
        Uniwind.setTheme(mode);
      }}
      value={saved}
    />
  );
}

Uniwind.setTheme throws on a name it does not know. Pass a stored string through restoreThemeMode, which falls back to "system", rather than straight to setTheme.

In a field

Inside a Field, the label names the group and isDisabled cascades.

In a field
In a field
<Field>
  <Field.Label>Appearance</Field.Label>
  <ThemeSelector variant="cards" />
  <Field.Description>System follows your device's light and dark setting.</Field.Description>
</Field>

useThemeSelection

The live mode, what it resolves to and a setter, with no selector mounted — for a custom control or a row that only shows the choice.

const { mode, resolved, setMode } = useThemeSelection();

Accessibility

The group is a radiogroup named by accessibilityLabel, the enclosing Field.Label, or "Theme"; each option is a radio announced by its label.

API Reference

ThemeSelector

Prop

Type

ThemeSelector.Option

Prop

Type

useThemeSelection

Prop

Type

restoreThemeMode

Prop

Type

On this page