SchemeSwitcher
Theme switcher / dark mode toggle: System, Light and Dark as SegmentedTabs, or one icon button cycling through them. Calls setScheme, which persists the choice.
Light
Dark
Your app is ready.
Inter body text, Inter headings.
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
npx nativecn-cli@latest add scheme-switcherAlso installs: theme, segmented-tabs, button.
Packages: lucide-react-native@^1.51.0, react-native-svg.
Usage
SchemeSwitcher demo
The segmented and icon Variants, sharing the persisted Scheme.
import { View } from "react-native";
import { SchemeSwitcher } from "@/components/scheme-switcher";
import { createStyles } from "@/theme";
const useStyles = createStyles((t) => ({
root: { gap: t.spacing[6] },
icon: { alignSelf: "flex-start" },
}));
// Both Variants share the persisted Scheme: changing one updates the other.
export default function SchemeSwitcherDemo() {
const styles = useStyles();
return (
<View style={styles.root}>
<SchemeSwitcher />
<SchemeSwitcher variant="icon" style={styles.icon} />
</View>
);
}SchemeSwitcher demo
The segmented and icon Variants, sharing the persisted Scheme.
import { View } from "react-native";
import { SchemeSwitcher } from "@/components/scheme-switcher";
import { createStyles } from "@/theme";
const useStyles = createStyles((t) => ({
root: { gap: t.spacing[6] },
icon: { alignSelf: "flex-start" },
}));
// Both Variants share the persisted Scheme: changing one updates the other.
export default function SchemeSwitcherDemo() {
const styles = useStyles();
return (
<View style={styles.root}>
<SchemeSwitcher />
<SchemeSwitcher variant="icon" style={styles.icon} />
</View>
);
}Props
| Prop | Type and description |
|---|---|
variant | "segmented" (default): System / Light / Dark tabs | "icon": one icon-only Button cycling System → Light → Dark |
disabled | boolean: blocks changes (default false) |
style | ViewStyle, layout only, merged last |
Variants
segmented,icon
Accessibility
- segmented: a tablist named "Theme" with System, Light and Dark tabs, aria-selected on the current one.
- icon: an icon-only Button named "Theme: <Scheme>" (e.g. "Theme: Dark"), with a 48 tap area.
- disabled is announced as disabled and blocks changes.
Notes
Lets the user choose the light/dark Scheme. Both Variants read schemePreference and call setScheme from useTheme(), which persists the choice (AsyncStorage, nativecn-theme) and makes native chrome follow; ThemeProvider must wrap the root Layout. segmented is SegmentedTabs with System (smartphone), Light (sun) and Dark (moon). icon is an icon-only ghost Button showing the current Scheme's icon; each press moves to the next Scheme. To switch from code instead, call setScheme('system' | 'light' | 'dark') from useTheme().
Source
The Vega Style's code, exactly as add copies it into your app.
{components}/scheme-switcher.tsx
import { Moon, Smartphone, Sun, type LucideIcon } from "lucide-react-native";
import type { Ref } from "react";
import type { StyleProp, View, ViewStyle } from "react-native";
import { Button } from "@/components/button";
import {
SegmentedTabs,
SegmentedTabsList,
SegmentedTabsTrigger,
} from "@/components/segmented-tabs";
import { useTheme, type SchemePreference } from "@/theme";
/** The three Schemes in cycling order, with their label and icon. */
const schemes = [
{ value: "system", label: "System", icon: Smartphone },
{ value: "light", label: "Light", icon: Sun },
{ value: "dark", label: "Dark", icon: Moon },
] as const satisfies readonly { value: SchemePreference; label: string; icon: LucideIcon }[];
/**
* - `segmented`: System · Light · Dark as SegmentedTabs.
* - `icon`: one icon-only Button showing the current Scheme; each press moves to the next.
*/
export type SchemeSwitcherVariant = "segmented" | "icon";
export type SchemeSwitcherProps = {
/** Default `segmented`. */
variant?: SchemeSwitcherVariant;
disabled?: boolean;
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
testID?: string;
ref?: Ref<View>;
};
/**
* Theme switcher / dark mode toggle: picks System, Light or Dark with the Theme's `setScheme`,
* which persists the choice (AsyncStorage, `nativecn-theme`) and makes native chrome follow.
*/
export function SchemeSwitcher({ variant = "segmented", disabled, ...props }: SchemeSwitcherProps) {
const { schemePreference, setScheme } = useTheme();
const index = schemes.findIndex((s) => s.value === schemePreference);
const current = schemes[index] ?? schemes[0];
if (variant === "icon") {
const next = schemes[(index + 1) % schemes.length]!;
return (
<Button
variant="ghost"
icon={current.icon}
aria-label={`Theme: ${current.label}`}
disabled={disabled}
onPress={() => setScheme(next.value)}
{...props}
/>
);
}
return (
<SegmentedTabs
value={schemePreference}
onValueChange={(value) => setScheme(value as SchemePreference)}
disabled={disabled}
{...props}
>
<SegmentedTabsList aria-label="Theme">
{schemes.map((s) => (
<SegmentedTabsTrigger key={s.value} value={s.value} label={s.label} icon={s.icon} />
))}
</SegmentedTabsList>
</SegmentedTabs>
);
}{components}/scheme-switcher.tsx
import { Moon, Smartphone, Sun, type LucideIcon } from "lucide-react-native";
import type { Ref } from "react";
import type { StyleProp, View, ViewStyle } from "react-native";
import { Button } from "@/components/button";
import {
SegmentedTabs,
SegmentedTabsList,
SegmentedTabsTrigger,
} from "@/components/segmented-tabs";
import { useTheme, type SchemePreference } from "@/theme";
/** The three Schemes in cycling order, with their label and icon. */
const schemes = [
{ value: "system", label: "System", icon: Smartphone },
{ value: "light", label: "Light", icon: Sun },
{ value: "dark", label: "Dark", icon: Moon },
] as const satisfies readonly { value: SchemePreference; label: string; icon: LucideIcon }[];
/**
* - `segmented`: System · Light · Dark as SegmentedTabs.
* - `icon`: one icon-only Button showing the current Scheme; each press moves to the next.
*/
export type SchemeSwitcherVariant = "segmented" | "icon";
export type SchemeSwitcherProps = {
/** Default `segmented`. */
variant?: SchemeSwitcherVariant;
disabled?: boolean;
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
testID?: string;
ref?: Ref<View>;
};
/**
* Theme switcher / dark mode toggle: picks System, Light or Dark with the Theme's `setScheme`,
* which persists the choice (AsyncStorage, `nativecn-theme`) and makes native chrome follow.
*/
export function SchemeSwitcher({ variant = "segmented", disabled, ...props }: SchemeSwitcherProps) {
const { schemePreference, setScheme } = useTheme();
const index = schemes.findIndex((s) => s.value === schemePreference);
const current = schemes[index] ?? schemes[0];
if (variant === "icon") {
const next = schemes[(index + 1) % schemes.length]!;
return (
<Button
variant="ghost"
icon={current.icon}
aria-label={`Theme: ${current.label}`}
disabled={disabled}
onPress={() => setScheme(next.value)}
{...props}
/>
);
}
return (
<SegmentedTabs
value={schemePreference}
onValueChange={(value) => setScheme(value as SchemePreference)}
disabled={disabled}
{...props}
>
<SegmentedTabsList aria-label="Theme">
{schemes.map((s) => (
<SegmentedTabsTrigger key={s.value} value={s.value} label={s.label} icon={s.icon} />
))}
</SegmentedTabsList>
</SegmentedTabs>
);
}
