Skip to main content
Version: v0.23.1

Picker sheets

Every React Native picker in this library is also available inside a BottomSheet with Cancel / Done: in the header beside the title on iOS, under the content with OK in Material, per the design. They all share the same "chrome" props and the same draft behaviour.

Opening a sheet from a form field?

You don't need your own button and visible state. Picker fields (DatePickerField, TimePickerField, DateTimePickerField, DurationPickerField, MonthPickerField, ColorPickerField, 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​

Platform
presentation
mode
display
<DatePickerSheet
visible={open}
onClose={() => setOpen(false)}
value={value}
onConfirm={setValue}
/>

How drafts work​

  1. When the sheet opens, it copies value into a draft.
  2. The picker inside edits only the draft.
  3. Done calls onConfirm(draft) and then onClose().
  4. 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):

PropTypeDefaultDescription
visiblebooleanrequiredShow / hide.
onClose() => voidrequiredSet visible to false here.
titleReactNode—Header title.
confirmLabelstring'Done' · 'OK' MaterialConfirm button text.
cancelLabelstring'Cancel'Cancel button text.
showCancelbooleantrue (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.
sheetPropsPartial<BottomSheetProps>—Passed to the BottomSheet (height, bottomInset, styles, enableSwipeToClose…).
themeDeepPartial<PickerTheme>—Theme for the sheet and the picker.
design'ios' | 'material' | 'web'from PickersProviderThe look to follow, over the provider's: iOS, Material or web.

DatePickerSheet​

A calendar (single, range, multiple) or date wheel in a sheet.

Loading…
<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 start and end in 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 at value or today. locale, minDate and maxDate are forwarded; pass other wheel props via wheelProps.

Props​

Shared props, plus every calendar prop (minDate, maxDate, isDateDisabled, markedDates, locale, firstDayOfWeek, styles, renderDay…), plus:

PropTypeDefaultDescription
mode'single' | 'range' | 'multiple' | 'ranges''single'Selection mode.
valueDate | null · DateRange · Date[] · DateRange[]—Current committed value; copied into the draft on open.
onConfirm(date: Date) · (range: DateRange) · (dates: Date[]) · (ranges: DateRange[]) => voidrequiredCalled with the draft on Done.
minRangeDays / maxRangeDaysnumber—Range and ranges modes.
maxSelectionsnumber—Multiple mode.
maxRangesnumber—Ranges mode.
display'calendar' | 'wheel''calendar'Wheel is single mode only.
wheelPropsPartial<DateWheelPickerProps>—Extra props for the wheel.
confirmOnSelectbooleanfalseSingle calendar: confirm on tap, no Done button.

TimePickerSheet​

Loading…
<>
  <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:

PropTypeDefaultDescription
valueDatenowCopied into the draft on open.
onConfirm(date: Date) => voidrequiredCalled with the draft on Done.

WheelPickerSheet​

Loading…
<>
  <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:

PropTypeDefaultDescription
valueTfirst itemCopied into the draft on open.
onConfirm(value: T) => voidrequiredCalled with the draft on Done.

EmojiPickerSheet​

No Done button: each pick calls onSelect, and the sheet closes unless closeOnSelect={false}.

Loading…
<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:

PropTypeDefaultDescription
closeOnSelectbooleantrueClose after a pick.
heightnumber420Picker height inside the sheet.
showCancelbooleanfalseShow 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.

Loading…
<>
  <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>
</>
PropTypeDefaultDescription
childrenReactNoderequiredSheet content.
onConfirm() => void—Shows the confirm button. You close the sheet (call onClose / set visible).
confirmDisabledbooleanfalseDisable the confirm button.
…shared propsvisible, 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.