- Accordion
- Action Sheet
- Alert Dialog
- Alert
- Audio Player
- Audio Recorder
- Audio Waveform
- Avatar
- AvoidKeyboard
- Badge
- BottomSheet
- Button
- Camera Preview
- Camera
- Card
- Carousel
- Checkbox
- Collapsible
- Color Picker
- Combobox
- Date Picker
- File Picker
- Gallery
- Hello Wave
- Icon
- Image
- Input OTP
- Input
- Link
- MediaPicker
- Mode Toggle
- Onboarding
- ParallaxScrollView
- Picker
- Popover
- Progress
- Radio
- ScrollView
- SearchBar
- Separator
- Share
- Sheet
- Skeleton
- Spinner
- Switch
- Table
- Tabs
- Text
- Toast
- Toggle
- Video
- View
Installation
pnpm dlx bna-ui add useModeToggle
Requirements
The mode lives in ModeProvider, not in this hook, so one
has to be mounted above every component that calls useModeToggle. The
ThemeProvider you already wrap your app in
mounts it for you — if you use that, there is nothing to do:
import { ThemeProvider } from '@/providers/theme-provider';
export default function RootLayout() {
return (
<ThemeProvider>
<Stack />
</ThemeProvider>
);
}Calling the hook without a provider throws rather than silently doing nothing.
Sharing the state this way is what makes the toggle behave: every toggle in the app agrees on the current mode, remounting a screen no longer resets the cycle, and the theme changes on web as well as native.
Usage
import { useModeToggle } from '@/hooks/useModeToggle';export function ThemeToggle() {
const { isDark, mode, setMode, toggleMode } = useModeToggle();
return (
<View>
<Text>Current mode: {mode}</Text>
<Text>Is dark: {isDark ? 'Yes' : 'No'}</Text>
<TouchableOpacity onPress={toggleMode}>
<Text>Toggle Theme</Text>
</TouchableOpacity>
</View>
);
}API Reference
useModeToggle
A hook that provides complete control over theme mode switching with support for light, dark, and system modes.
Returns
| Property | Type | Description |
|---|---|---|
isDark | boolean | Whether the current effective theme is dark |
mode | 'light' | 'dark' | 'system' | The currently selected mode setting |
setMode | (mode: Mode) => void | Function to set a specific mode |
currentMode | 'light' | 'dark' | The resolved color scheme, with 'system' already applied |
toggleMode | () => void | Function to cycle through modes: light → dark → system → light |
Type Definitions
type Mode = 'light' | 'dark' | 'system';
interface UseModeToggleReturn {
isDark: boolean;
mode: Mode;
setMode: (mode: Mode) => void;
currentMode: 'light' | 'dark';
toggleMode: () => void;
}Mode Behavior
Setting a mode writes it to ModeProvider, which every useColorScheme call in
the app reads — that is what repaints your components. On native the choice is
additionally mirrored into React Native's global Appearance, so the status
bar, the Android navigation bar and native sheets follow along.
Light Mode
- Forces the app to use light theme regardless of system preference
isDarkreturnsfalsecurrentModereturns'light'
Dark Mode
- Forces the app to use dark theme regardless of system preference
isDarkreturnstruecurrentModereturns'dark'
System Mode
- Follows the system's color scheme preference
isDarkreflects the actual system preferencecurrentModereturns the system's current preference
Toggle Cycle
The toggleMode function cycles through modes in this order:
light → dark → system → light → ...
This provides an intuitive way for users to quickly switch between all available options.
Use Cases
This hook is perfect for:
- Creating theme toggle buttons in settings screens
- Building comprehensive theme selection interfaces
- Implementing persistent theme preferences
- Providing users with granular control over app appearance
- Creating theme-aware components that need to know the current mode
- Building accessibility-compliant theme switching
Best Practices
Settings Screen Implementation
Create a comprehensive settings screen with theme options:
export function ThemeSettings() {
const { mode, setMode, isDark, currentMode } = useModeToggle();
const options = [
{ key: 'light', label: 'Light', icon: '☀️' },
{ key: 'dark', label: 'Dark', icon: '🌙' },
{ key: 'system', label: 'System', icon: '⚙️' },
];
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 16 }}>Theme</Text>
{options.map((option) => (
<TouchableOpacity
key={option.key}
style={{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
backgroundColor: mode === option.key ? '#007AFF20' : 'transparent',
borderRadius: 8,
paddingHorizontal: 12,
}}
onPress={() => setMode(option.key as Mode)}
>
<Text style={{ marginRight: 12 }}>{option.icon}</Text>
<Text style={{ flex: 1 }}>{option.label}</Text>
{mode === option.key && <Text>✓</Text>}
</TouchableOpacity>
))}
<Text style={{ marginTop: 16, opacity: 0.6 }}>
Current: {currentMode} (Effective: {isDark ? 'dark' : 'light'})
</Text>
</View>
);
}Persistent Theme Storage
Persistence is a prop on the provider, not something to wrap this hook in. Pass
any key/value store — expo-secure-store matches the shape as-is, and is what
every npx bna-ui init scaffold uses:
import * as SecureStore from 'expo-secure-store';
import { ThemeProvider } from '@/providers/theme-provider';
export default function RootLayout() {
return (
<ThemeProvider storage={SecureStore}>
<Stack />
</ThemeProvider>
);
}The mode is restored on mount and written on every change. A missing or
unreadable value falls back to defaultMode, so a failed read can never block
startup — including on web, where SecureStore is unavailable and persistence
simply no-ops. See ModeProvider for the full contract.
Animated Theme Toggle
Create smooth transitions between theme modes:
import { useEffect, useRef } from 'react';
import { Animated } from 'react-native';
export function AnimatedThemeToggle() {
const { isDark, toggleMode } = useModeToggle();
const animatedValue = useRef(new Animated.Value(isDark ? 1 : 0)).current;
useEffect(() => {
Animated.timing(animatedValue, {
toValue: isDark ? 1 : 0,
duration: 300,
useNativeDriver: false,
}).start();
}, [isDark]);
const backgroundColor = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: ['#ffffff', '#000000'],
});
return (
<Animated.View style={{ backgroundColor, flex: 1 }}>
<TouchableOpacity onPress={toggleMode}>
<Text>Toggle Theme</Text>
</TouchableOpacity>
</Animated.View>
);
}Header Integration
Integrate theme toggle into your app header:
export function AppHeader() {
const { mode, toggleMode } = useModeToggle();
const getToggleIcon = () => {
switch (mode) {
case 'light':
return '☀️';
case 'dark':
return '🌙';
case 'system':
return '⚙️';
}
};
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
}}
>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>My App</Text>
<TouchableOpacity onPress={toggleMode}>
<Text style={{ fontSize: 20 }}>{getToggleIcon()}</Text>
</TouchableOpacity>
</View>
);
}Performance Considerations
Memoization
The hook uses internal state management and is already optimized, but you can memoize dependent calculations:
const themeStyles = useMemo(
() => ({
container: {
backgroundColor: isDark ? '#000' : '#fff',
color: isDark ? '#fff' : '#000',
},
}),
[isDark]
);Avoiding Unnecessary Re-renders
Only destructure the values you actually need:
// Good: Only get what you need
const { isDark, toggleMode } = useModeToggle();
// Less optimal: Getting all values when you only need some
const modeToggle = useModeToggle();Advanced Usage
Custom Mode Validation
Add validation for supported modes:
export function useValidatedModeToggle() {
const modeToggle = useModeToggle();
const setModeWithValidation = (mode: string) => {
if (['light', 'dark', 'system'].includes(mode)) {
modeToggle.setMode(mode as Mode);
} else {
console.warn(`Invalid mode: ${mode}`);
}
};
return {
...modeToggle,
setMode: setModeWithValidation,
};
}Reading the Mode Without the Toggle
You do not need a context of your own — the mode already lives in one. For
components that only read the theme, use
useColorScheme or
useColor; both resolve through the same provider. To
read or set the mode without the light → dark → system cycle, reach for
useModeContext:
import { useModeContext } from '@/providers/mode-provider';
export function ThemeLabel() {
const { mode, scheme } = useModeContext() ?? {};
return (
<Text>
{mode} (rendering as {scheme})
</Text>
);
}Dependencies
@/providers/mode-provider- Holds the mode and resolves it to a schemereact- Required for context
Accessibility
The hook supports accessibility by:
- Providing programmatic access to theme state for screen readers
- Enabling proper contrast ratios through theme mode control
- Supporting system-level accessibility preferences in system mode
- Allowing users to override system preferences when needed
Platform Support
All three modes work on every platform, because the mode is held in React context rather than pushed through a platform API.
iOS
- System mode respects iOS system preferences
- The choice is mirrored into
Appearance.setColorScheme, so the status bar and native sheets follow
Android
- System mode respects Android system preferences
- The choice is mirrored into
Appearance.setColorScheme, so the status bar and the navigation bar follow
Web
- System mode detection works through
useColorScheme - react-native-web's
Appearanceis read-only — it hasgetColorSchemeandaddChangeListenerbut no setter — so the provider is the only thing driving the theme here. That is by design, and why the toggle works on web
Related Hooks
useColorScheme- Base hook for theme detectionuseColor- For color resolution based on themeModeProvider- Holds the mode this hook reads and writes
On This Page
InstallationRequirementsUsageAPI ReferenceuseModeToggleReturnsType DefinitionsMode BehaviorLight ModeDark ModeSystem ModeToggle CycleUse CasesBest PracticesSettings Screen ImplementationPersistent Theme StorageAnimated Theme ToggleHeader IntegrationPerformance ConsiderationsMemoizationAvoiding Unnecessary Re-rendersAdvanced UsageCustom Mode ValidationReading the Mode Without the ToggleDependenciesAccessibilityPlatform SupportiOSAndroidWebRelated Hooks