Theme Picker
A segmented control highlighted by `preference`, so the `'system'` row can win.
A segmented control where the active option is highlighted. The
highlight reads preference (not theme.name) so the 'system'
row can win when the user is following the OS. Because preference
updates synchronously inside setTheme, there's no need for local
picked state. The highlight repaints in the same commit as the
tap.
Example
import { Pressable, Text, View } from 'react-native'
import { useTheme } from '@/lib/theme'
const OPTIONS = ['system', 'light', 'dark'] as const
export function ThemePicker() {
const { theme, preference, setTheme, isTransitioning } = useTheme()
return (
<View style={{ flexDirection: 'row', gap: 8 }}>
{OPTIONS.map((option) => (
<Pressable
key={option}
disabled={isTransitioning}
onPress={() => setTheme(option)}
style={{
flex: 1,
padding: 12,
borderRadius: 8,
alignItems: 'center',
backgroundColor:
preference === option ? theme.colors.primary : 'transparent',
}}
>
<Text
style={{ color: preference === option ? '#fff' : theme.colors.text }}
>
{option}
</Text>
</Pressable>
))}
</View>
)
}Why preference and Not theme.name
theme.name is always the concrete painted theme, never 'system'.
If you drive the highlight from it, the 'system' row can never be
selected: the library always resolves 'system' to a real theme like
'light' or 'dark', and that's what theme.name would carry.
preference mirrors the raw value the user passed to setTheme (or
the initial initialTheme prop). It updates synchronously when
setTheme runs, so the highlight repaints in the same commit. The
engine waits one frame before capturing, giving React time to paint
the new selection.
With Persistence
Call setTheme and your store setter together in onPress. Don't
route through a reactive bridge; it'll race with the settle window.
import { useThemeStore } from '@/stores/theme-store'
export function ThemePicker() {
const { theme, preference, setTheme, isTransitioning } = useTheme()
const setColorMode = useThemeStore((s) => s.setColorMode)
return (
<View style={{ flexDirection: 'row', gap: 8 }}>
{OPTIONS.map((option) => (
<Pressable
key={option}
disabled={isTransitioning}
onPress={() => {
setTheme(option)
setColorMode(option)
}}
style={{
flex: 1,
padding: 12,
borderRadius: 8,
alignItems: 'center',
backgroundColor:
preference === option ? theme.colors.primary : 'transparent',
}}
>
<Text
style={{ color: preference === option ? '#fff' : theme.colors.text }}
>
{option}
</Text>
</Pressable>
))}
</View>
)
}See State Managers for the hydration pattern and Persisted Preference for the AsyncStorage-only version.
See Also
- Checkmark List. iOS Settings-style list using the same pattern.
- Theme Toggle. Binary dark/light switch.
- useTheme. The hook reference.