Theming
Pickers ship with a polished light and dark theme that follow the OS appearance automatically. Change any token for the whole app with PickersProvider, or for one component with its theme prop.
PickersProvider
import { PickersProvider } from '@elik56a/react-native-pickers';
export default function App() {
return (
<PickersProvider
colorScheme="auto"
theme={{
colors: { primary: '#FF5A5F', primaryMuted: 'rgba(255,90,95,0.12)' },
radius: { md: 8, sheet: 16 },
typography: { fontFamily: 'Inter-Regular', fontFamilyMedium: 'Inter-SemiBold' },
}}
darkTheme={{
colors: { primary: '#FF8A8E', background: '#000000', sheet: '#111111' },
}}
>
<Navigation />
</PickersProvider>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
colorScheme | 'auto' | 'light' | 'dark' | 'auto' | 'auto' follows useColorScheme(). |
theme | DeepPartial<PickerTheme> | — | Overrides merged onto lightTheme. Also applied to dark mode when darkTheme is not given. |
darkTheme | DeepPartial<PickerTheme> | — | Overrides merged onto darkTheme. |
children | ReactNode | required |
If you pass only theme, the same overrides are applied on top of the dark base too. That's right for brand colours and fonts, but wrong for backgrounds. Pass darkTheme whenever you override surface colours.
Providers can be nested; the innermost one wins. That's an easy way to force one area dark:
import { useState } from 'react'; import { View } from 'react-native'; import { PickersProvider, DatePicker } from '@elik56a/react-native-pickers'; export default function Example() { const [date, setDate] = useState<Date | null>(new Date()); return ( <PickersProvider colorScheme="dark"> <View style={{ backgroundColor: '#181612', borderRadius: 20, padding: 8 }}> <DatePicker value={date} onChange={setDate} /> </View> </PickersProvider> ); }
Per-component theme
Every component accepts theme, merged on top of the provider's theme for that component only:
import { useState } from 'react'; import { View } from 'react-native'; import { TimePicker } from '@elik56a/react-native-pickers'; export default function Example() { const [t, setT] = useState(new Date()); return ( <View style={{ gap: 16 }}> <TimePicker value={t} onChange={setT} theme={{ colors: { indicator: 'rgba(16,185,129,0.15)' }, typography: { sizes: { lg: 24 } } }} /> <TimePicker value={t} onChange={setT} theme={{ colors: { indicator: 'rgba(244,63,94,0.12)', text: '#E11D48' }, radius: { md: 0 } }} /> </View> ); }
Pass a constant or useMemo'd object rather than an inline literal, so the merged theme isn't recomputed every render. It matters most on wheels.
Tokens
interface PickerTheme {
dark: boolean;
colors: {
background: string; // picker surfaces
surface: string; // search inputs, chips, pressed rows
sheet: string; // bottom sheet background
text: string;
textSecondary: string; // labels, unselected wheel rows, placeholders
textDisabled: string;
primary: string; // selected day, check marks, buttons, accents
onPrimary: string; // text on primary
primaryMuted: string; // range band, selected list row, chips
border: string;
backdrop: string; // sheet overlay
indicator: string; // wheel selection band
handle: string; // sheet grab handle
hover: string; // tint while the pointer is over anything pressable
ripple: string; // ripple that spreads from the touch point on press
};
radius: { sm: number; md: number; lg: number; full: number; sheet: number };
spacing: { xs: number; sm: number; md: number; lg: number; xl: number };
typography: {
fontFamily?: string;
fontFamilyMedium?: string; // used for medium and bold weights; falls back to fontFamily
sizes: { xs: number; sm: number; md: number; lg: number; xl: number };
weights: { regular: TextStyle['fontWeight']; medium: TextStyle['fontWeight']; bold: TextStyle['fontWeight'] };
};
animation: { duration: number }; // ms: month slide, sheet open (+60) / close
}
Defaults
| Token | Light | Dark |
|---|---|---|
colors.background | #FFFFFF | #181612 |
colors.surface | #F4F2EE | #24211B |
colors.sheet | #FFFFFF | #1D1B16 |
colors.text | #17150F | #F5F2EC |
colors.textSecondary | #6E685E | #A39D92 |
colors.textDisabled | #BDB7AC | #534E45 |
colors.primary | #C8460D | #FF7A3D |
colors.onPrimary | #FFFFFF | #17150F |
colors.primaryMuted | rgba(200,70,13,0.12) | rgba(255,122,61,0.18) |
colors.border | #E8E4DC | #2F2B24 |
colors.backdrop | rgba(23,21,15,0.45) | rgba(0,0,0,0.6) |
colors.indicator | rgba(23,21,15,0.06) | rgba(255,255,255,0.07) |
colors.handle | #D8D3C9 | #3D3930 |
colors.hover | rgba(23,21,15,0.05) | rgba(255,255,255,0.06) |
colors.ripple | rgba(23,21,15,0.18) | rgba(255,255,255,0.2) |
| Token | Value (both) |
|---|---|
radius | sm 8 · md 12 · lg 16 · full 999 · sheet 24 |
spacing | xs 4 · sm 8 · md 12 · lg 16 · xl 24 |
typography.sizes | xs 11 · sm 13 · md 16 · lg 19 · xl 22 |
typography.weights | regular '400' · medium '500' · bold '600' |
animation.duration | 240 |
Where each colour is used
| Token | Used by |
|---|---|
primary | Selected day, today ring, calendar arrows, check marks, chips text, Done button, category underline, tap-to-type ring |
primaryMuted | Range band, selected select row, chips background, selected skin tone |
indicator | Wheel selection band |
surface | Search inputs, skin-tone button |
hover | Every pressable while the pointer is over it: field, rows, days, wheel rows, emoji, buttons |
ripple | The same elements on press, on every platform, except the select field and wheel rows |
textSecondary | Unselected wheel rows, weekday names, placeholders, descriptions, Cancel |
textDisabled | Disabled days / rows, outside-month days |
Hover and ripple
Everything you can press (the select field, list and dropdown rows, calendar days, wheel rows, emoji, sheet buttons) tints while a pointer is over it. Items also ripple from the touch point when pressed; the select field and wheel rows don't, since the field opens a list and a wheel row scrolls into place. Hover only happens with a mouse or trackpad, so phones get just the ripple. Both are theme colours, so they follow light and dark mode:
import { useState } from 'react'; import { SelectList } from '@elik56a/react-native-pickers'; const fruits = ['Apple', 'Banana', 'Cherry', 'Mango'].map((f) => ({ label: f, value: f })); export default function Example() { const [fruit, setFruit] = useState<string | null>('Banana'); return ( <SelectList options={fruits} value={fruit} onChange={setFruit} theme={{ colors: { hover: 'rgba(200,70,13,0.08)', ripple: 'rgba(200,70,13,0.22)' } }} /> ); }
Set either to 'transparent' to turn that effect off. Hover and press both lists to compare:
import { useState } from 'react'; import { Text, View } from 'react-native'; import { SelectList } from '@elik56a/react-native-pickers'; const sizes = ['Small', 'Medium', 'Large'].map((s) => ({ label: s, value: s })); export default function Example() { const [a, setA] = useState<string | null>('Medium'); const [b, setB] = useState<string | null>('Medium'); return ( <View style={{ flexDirection: 'row', gap: 16 }}> <View style={{ flex: 1 }}> <Text style={{ fontWeight: '600', marginBottom: 8 }}>Default</Text> <SelectList options={sizes} value={a} onChange={setA} /> </View> <View style={{ flex: 1 }}> <Text style={{ fontWeight: '600', marginBottom: 8 }}>Turned off</Text> <SelectList options={sizes} value={b} onChange={setB} theme={{ colors: { hover: 'transparent', ripple: 'transparent' } }} /> </View> </View> ); }
For the whole app, set them once on the provider:
<PickersProvider theme={{ colors: { hover: 'transparent', ripple: 'transparent' } }}>…</PickersProvider>
Dark mode has its own defaults (a light tint instead of a dark one); override them through darkTheme:
<PickersProvider
theme={{ colors: { hover: 'rgba(14,165,233,0.08)', ripple: 'rgba(14,165,233,0.2)' } }}
darkTheme={{ colors: { hover: 'rgba(56,189,248,0.12)', ripple: 'rgba(56,189,248,0.25)' } }}
>
…
</PickersProvider>
Building a theme in code
import { createTheme, mergeTheme, lightTheme, darkTheme } from '@elik56a/react-native-pickers';
// Full theme from overrides on top of light (or dark when overrides.dark is true)
const brand = createTheme({ colors: { primary: '#0EA5E9' } });
// Explicit base
const brandDark = createTheme({ colors: { primary: '#38BDF8' } }, darkTheme);
// Generic deep merge (objects are merged, arrays and primitives replaced)
const tweaked = mergeTheme(lightTheme, { radius: { md: 4 } });
Using the theme in your own components
usePickerTheme() returns the active, resolved theme, so your buttons and fields can match the pickers:
import { usePickerTheme, textStyle } from '@elik56a/react-native-pickers';
function DoneButton({ onPress }: { onPress: () => void }) {
const theme = usePickerTheme();
return (
<Pressable onPress={onPress} style={{ backgroundColor: theme.colors.primary, borderRadius: theme.radius.md, padding: theme.spacing.md }}>
<Text style={[textStyle(theme, 'md', 'bold'), { color: theme.colors.onPrimary }]}>Done</Text>
</Pressable>
);
}
textStyle(theme, size, weight?) returns { fontSize, fontWeight, fontFamily, color } from the tokens.
Matching your design system
Map your existing tokens once:
import { colors, radii, fonts } from './design-system';
const pickerTheme = {
colors: { primary: colors.brand500, primaryMuted: colors.brand50, text: colors.gray900, textSecondary: colors.gray500, border: colors.gray200 },
radius: { md: radii.md, sheet: radii.xl },
typography: { fontFamily: fonts.regular, fontFamilyMedium: fonts.semibold },
};
const pickerDarkTheme = { colors: { primary: colors.brand300, background: colors.gray950, sheet: colors.gray900 } };
<PickersProvider theme={pickerTheme} darkTheme={pickerDarkTheme}>…</PickersProvider>
→ For per-part tweaks beyond tokens, see Styling.