Picker sheets
Every React Native picker in this library is also available inside a BottomSheet with a Cancel / Done header. They all share the same "chrome" props and the same draft behaviour.
You don't need your own button and visible state. Picker fields (DatePickerField, TimePickerField, DateTimePickerField, WheelPickerField, EmojiPickerField) take the same props without visible/onClose, render a field that opens the sheet, and show the confirmed value.
Inline, bottom sheet or modalβ
Every picker can be shown three ways:
- Inline: the plain component (
WheelPicker,TimePicker,DatePicker,SelectList,EmojiPicker) renders in place. - Bottom sheet: slides up from the bottom and can be dragged down to dismiss.
- Modal: a centered dialog.
Each sheet can also open from a field that shows the picked value.
The sheets take presentation: 'auto' (default), 'bottomSheet' or 'modal'. auto is a modal on web windows wider than the sheet, where a bottom sheet would look cut off, and a bottom sheet on phones and native.
<TimePickerSheet presentation="modal" visible={open} onClose={close} value={time} onConfirm={setTime} />
SelectPicker also supports 'dropdown', which opens under the field. It's SelectPicker's default on the web.
import { DatePickerSheet, TimePickerSheet, WheelPickerSheet, EmojiPickerSheet, PickerSheet, useDraft } from '@elik56a/react-native-pickers';
Try the propsβ
presentationmodedisplay<DatePickerSheet
visible={open}
onClose={() => setOpen(false)}
value={value}
onConfirm={setValue}
/>
How drafts workβ
- When the sheet opens, it copies
valueinto a draft. - The picker inside edits only the draft.
- Done calls
onConfirm(draft)and thenonClose(). - Cancel, a backdrop tap, a swipe down or Android back calls only
onClose(), and the draft is thrown away.
Your state changes only when the user confirms, which is how native iOS / Android pickers behave.
Shared propsβ
All sheets accept these (SheetChromeProps):
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | required | Show / hide. |
onClose | () => void | required | Set visible to false here. |
title | ReactNode | β | Header title. |
confirmLabel | string | 'Done' | Confirm button text. |
cancelLabel | string | 'Cancel' | Cancel button text. |
showCancel | boolean | true (false for emoji) | Show the Cancel button. |
presentation | 'auto' | 'bottomSheet' | 'modal' | 'auto' | Bottom sheet or centered modal. auto: modal on wide web windows, bottom sheet elsewhere. |
sheetProps | Partial<BottomSheetProps> | β | Passed to the BottomSheet (height, bottomInset, styles, enableSwipeToCloseβ¦). |
theme | DeepPartial<PickerTheme> | β | Theme for the sheet and the picker. |
DatePickerSheetβ
A calendar (single, range, multiple) or date wheel in a sheet.
import { useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import { DatePickerSheet, formatDate } from '@elik56a/react-native-pickers'; import type { DateRange } from '@elik56a/react-native-pickers'; function Field({ label, value, onPress }: { label: string; value: string; onPress: () => void }) { return ( <Pressable onPress={onPress} style={{ borderWidth: 1, borderColor: '#E8E4DC', borderRadius: 12, padding: 14 }}> <Text style={{ fontSize: 12, color: '#6E685E' }}>{label}</Text> <Text style={{ fontSize: 16, marginTop: 2 }}>{value}</Text> </Pressable> ); } export default function Example() { const [sheet, setSheet] = useState<'day' | 'range' | 'birthday' | null>(null); const [day, setDay] = useState<Date | null>(null); const [range, setRange] = useState<DateRange>({ start: null, end: null }); const [birthday, setBirthday] = useState(new Date(1995, 6, 12)); const close = () => setSheet(null); return ( <View style={{ gap: 10 }}> <Field label="Delivery date" value={day ? formatDate(day) : 'Chooseβ¦'} onPress={() => setSheet('day')} /> <Field label="Trip" value={range.start && range.end ? `${formatDate(range.start)} β ${formatDate(range.end)}` : 'Chooseβ¦'} onPress={() => setSheet('range')} /> <Field label="Birthday" value={formatDate(birthday)} onPress={() => setSheet('birthday')} /> <DatePickerSheet visible={sheet === 'day'} onClose={close} title="Delivery" value={day} onConfirm={setDay} minDate={new Date()} confirmOnSelect /> <DatePickerSheet visible={sheet === 'range'} onClose={close} title="Trip dates" mode="range" value={range} onConfirm={setRange} minDate={new Date()} /> <DatePickerSheet visible={sheet === 'birthday'} onClose={close} title="Birthday" display="wheel" value={birthday} onConfirm={setBirthday} maxDate={new Date()} /> </View> ); }
Behaviourβ
- Done is disabled until the selection is complete: a date in single mode, both
startandendin range mode, no half-picked range in ranges mode. Multiple and ranges modes can confirm an empty array. confirmOnSelect(single + calendar only) hides Done and confirms as soon as a day is tapped.display="wheel"shows a DateWheelPicker (single mode). The draft starts atvalueor today.locale,minDateandmaxDateare forwarded; pass other wheel props viawheelProps.
Propsβ
Shared props, plus every calendar prop (minDate, maxDate, isDateDisabled, markedDates, locale, firstDayOfWeek, styles, renderDayβ¦), plus:
| Prop | Type | Default | Description |
|---|---|---|---|
mode | 'single' | 'range' | 'multiple' | 'ranges' | 'single' | Selection mode. |
value | Date | null Β· DateRange Β· Date[] Β· DateRange[] | β | Current committed value; copied into the draft on open. |
onConfirm | (date: Date) Β· (range: DateRange) Β· (dates: Date[]) Β· (ranges: DateRange[]) => void | required | Called with the draft on Done. |
minRangeDays / maxRangeDays | number | β | Range and ranges modes. |
maxSelections | number | β | Multiple mode. |
maxRanges | number | β | Ranges mode. |
display | 'calendar' | 'wheel' | 'calendar' | Wheel is single mode only. |
wheelProps | Partial<DateWheelPickerProps> | β | Extra props for the wheel. |
confirmOnSelect | boolean | false | Single calendar: confirm on tap, no Done button. |
TimePickerSheetβ
import { useState } from 'react'; import { Pressable, Text } from 'react-native'; import { TimePickerSheet } from '@elik56a/react-native-pickers'; export default function Example() { const [open, setOpen] = useState(false); const [time, setTime] = useState(new Date(2026, 0, 1, 7, 30)); return ( <> <Pressable onPress={() => setOpen(true)} style={{ alignItems: 'center', padding: 12 }}> <Text style={{ fontSize: 48, fontWeight: '200' }}>{time.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}</Text> <Text style={{ color: '#C8460D' }}>Change</Text> </Pressable> <TimePickerSheet visible={open} onClose={() => setOpen(false)} title="Wake up" value={time} onConfirm={setTime} minuteInterval={5} /> </> ); }
Props: shared props, every TimePicker prop except value/defaultValue/onChange, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
value | Date | now | Copied into the draft on open. |
onConfirm | (date: Date) => void | required | Called with the draft on Done. |
WheelPickerSheetβ
import { useState } from 'react'; import { Pressable, Text } from 'react-native'; import { WheelPickerSheet } from '@elik56a/react-native-pickers'; const guests = Array.from({ length: 12 }, (_, i) => ({ label: `${i + 1} guest${i ? 's' : ''}`, value: i + 1 })); export default function Example() { const [open, setOpen] = useState(false); const [count, setCount] = useState(2); return ( <> <Pressable onPress={() => setOpen(true)} style={{ borderWidth: 1, borderColor: '#E8E4DC', borderRadius: 12, padding: 14 }}> <Text>π₯ {guests[count - 1].label}</Text> </Pressable> <WheelPickerSheet visible={open} onClose={() => setOpen(false)} title="Guests" items={guests} value={count} onConfirm={setCount} /> </> ); }
Props: shared props, every WheelPicker prop except value/defaultValue/onChange, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
value | T | first item | Copied into the draft on open. |
onConfirm | (value: T) => void | required | Called with the draft on Done. |
EmojiPickerSheetβ
No Done button: each pick calls onSelect, and the sheet closes unless closeOnSelect={false}.
import { useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import { EmojiPickerSheet } from '@elik56a/react-native-pickers'; export default function Example() { const [open, setOpen] = useState(false); const [reactions, setReactions] = useState(['π', 'π']); return ( <View style={{ flexDirection: 'row', gap: 8, flexWrap: 'wrap' }}> {reactions.map((r, i) => ( <Text key={i} style={{ fontSize: 22, backgroundColor: '#F4F2EE', borderRadius: 999, paddingHorizontal: 10, paddingVertical: 4 }}>{r}</Text> ))} <Pressable onPress={() => setOpen(true)} style={{ borderRadius: 999, borderWidth: 1, borderColor: '#E8E4DC', paddingHorizontal: 12, justifyContent: 'center' }}> <Text>οΌ</Text> </Pressable> <EmojiPickerSheet visible={open} onClose={() => setOpen(false)} title="React" onSelect={(e) => setReactions((r) => [...r, e])} /> </View> ); }
Props: shared props, every EmojiPicker prop, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
closeOnSelect | boolean | true | Close after a pick. |
height | number | 420 | Picker height inside the sheet. |
showCancel | boolean | false | Show a Cancel button. |
PickerSheetβ
The building block: a BottomSheet with Cancel on the left and (when you pass onConfirm) Done on the right. Use it to build sheets for your own pickers.
import { useState } from 'react'; import { Pressable, Text, Switch, View } from 'react-native'; import { PickerSheet, useDraft } from '@elik56a/react-native-pickers'; type Prefs = { push: boolean; email: boolean }; export default function Example() { const [open, setOpen] = useState(false); const [prefs, setPrefs] = useState<Prefs>({ push: true, email: false }); const [draft, setDraft] = useDraft(prefs, open); return ( <> <Pressable onPress={() => setOpen(true)} style={{ padding: 14, borderRadius: 12, backgroundColor: '#F4F2EE' }}> <Text>Notifications: {[prefs.push && 'push', prefs.email && 'email'].filter(Boolean).join(' + ') || 'off'}</Text> </Pressable> <PickerSheet visible={open} onClose={() => setOpen(false)} title="Notifications" confirmLabel="Save" onConfirm={() => { setPrefs(draft); setOpen(false); }} > <View style={{ padding: 16, gap: 16 }}> {(['push', 'email'] as const).map((k) => ( <View key={k} style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <Text style={{ fontSize: 16, textTransform: 'capitalize' }}>{k}</Text> <Switch value={draft[k]} onValueChange={(v) => setDraft({ ...draft, [k]: v })} /> </View> ))} </View> </PickerSheet> </> ); }
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Sheet content. |
onConfirm | () => void | β | Shows the confirm button. You close the sheet (call onClose / set visible). |
confirmDisabled | boolean | false | Disable the confirm button. |
| β¦shared props | visible, onClose, title, confirmLabel, cancelLabel, showCancel, sheetProps, theme. |
useDraftβ
function useDraft<V>(value: V, visible: boolean): [draft: V, setDraft: (v: V) => void];
Returns a draft copy of value that resets to value each time visible turns true. It's the hook the built-in sheets use; use it for your own PickerSheets as shown above.