Core concepts
Every component follows the same handful of conventions. Learn them once and you know the whole library.
Controlled and uncontrolled
Wheels, time, date and emoji state can be used either way:
// Controlled: you own the state
const [date, setDate] = useState(new Date());
<TimePicker value={date} onChange={setDate} />
// Uncontrolled: the component owns it; you only listen
<TimePicker defaultValue={new Date()} onChange={(d) => console.log(d)} />
| Component | Controlled | Uncontrolled |
|---|---|---|
WheelPicker | value | defaultValue |
TimePicker | value | defaultValue (default: now) |
DatePicker / Calendar | value | defaultValue |
DateWheelPicker | value | defaultValue (default: today) |
EmojiPicker skin tone | skinTone | defaultSkinTone |
EmojiPicker recents | recent | defaultRecent |
SelectPicker / SelectList | value | — (always controlled) |
SelectPicker and SelectList don't keep their own selection. Always pass value and update it in onChange.
Setting value from outside (for example, a "Reset" button) moves the component without firing onChange. onChange only reports changes the user made.
onChange vs onValueChanging
Wheel-based pickers (WheelPicker, MultiWheelPicker columns, TimePicker, DateWheelPicker) fire two callbacks:
| Callback | When | Use it for |
|---|---|---|
onChange | Once, after the wheel settles on a new value | Updating state, saving |
onValueChanging | On every row crossing while scrolling | Haptics, live previews |
<WheelPicker
items={items}
value={value}
onChange={setValue}
onValueChanging={() => Haptics.selectionAsync()}
/>
Sheets and drafts
DatePickerSheet, TimePickerSheet and WheelPickerSheet wrap a picker in a BottomSheet with Cancel and Done:
- The sheet copies
valueinto a draft each time it opens. - Scrolling or tapping changes only the draft.
- Done calls
onConfirm(draft), thenonClose(). - Cancel, a backdrop tap, a swipe down or Android back calls
onClose()only, so the draft is discarded.
<TimePickerSheet visible={open} onClose={() => setOpen(false)} value={time} onConfirm={setTime} />
EmojiPickerSheet has no Done button: picking an emoji calls onSelect and (by default) closes the sheet. SelectPicker opens its own sheet; you don't manage visible.
Fields
In a form, the picker hides behind a field. SelectPicker is one, and every sheet has a field version that takes the same props without visible / onClose: DatePickerField, TimePickerField, DateTimePickerField, WheelPickerField, EmojiPickerField. The field owns the open state, shows the confirmed value formatted for locale, and looks the same as SelectPicker's field.
<TimePickerField label="Alarm" value={time} onConfirm={setTime} />
Theming
Three layers, each merged on top of the previous one:
- Built-in
lightTheme/darkTheme, chosen from the OS appearance <PickersProvider theme={…} darkTheme={…}>for your whole app- The
themeprop on any single component
<PickersProvider theme={{ colors: { primary: '#0EA5E9' }, radius: { md: 6 } }}>
<DatePicker theme={{ colors: { primary: '#22C55E' } }} /> {/* green only here */}
</PickersProvider>
Slot styles
Every visual part of a component has a named slot. Pass styles for any of them through styles:
<DatePicker
styles={{
selectedDay: { borderRadius: 8 },
headerTitle: { fontSize: 20 },
rangeBand: { backgroundColor: '#FFE4E6' },
}}
/>
Slot names are typed, so your editor autocompletes them. Each component page lists its slots. → Styling guide
Render props
When styles aren't enough, replace a part entirely:
| Component | Render props |
|---|---|
WheelPicker | renderItem, renderIndicator |
DatePicker / Calendar | renderDay, renderArrow, formatTitle |
SelectPicker / SelectList | renderTrigger, renderOption, renderGroupHeader |
EmojiPicker | renderEmoji, categoryIcons |
*PickerField / PickerField | renderTrigger, renderChevron |
Strings and i18n
No component hard-codes a user-facing string you can't change. Done, Cancel, placeholders, search hints, empty states and category labels are all props. Month, weekday and AM/PM names come from Intl for whatever locale you pass. → i18n & RTL