Skip to main content
Version: v0.3.x

Picker sheets

Every picker is also available inside a BottomSheet with a Cancel / Done header. They all share the same "chrome" props and the same draft behaviour.

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.

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​

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'Confirm 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.

DatePickerSheet​

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

Result
Loading...
Live Editor
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 start and end in range mode. Multiple mode is always confirmable (including 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''single'Selection mode.
valueDate | null · DateRange · Date[]—Current committed value; copied into the draft on open.
onConfirm(date: Date) · (range: DateRange) · (dates: Date[]) => voidrequiredCalled with the draft on Done.
minRangeDays / maxRangeDaysnumber—Range mode.
maxSelectionsnumber—Multiple 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​

Result
Loading...
Live Editor
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:

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

WheelPickerSheet​

Result
Loading...
Live Editor
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:

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}.

Result
Loading...
Live Editor
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:

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.

Result
Loading...
Live Editor
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>
    </>
  );
}
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.