Skip to main content
Version: v0.23.1

SelectPicker

SelectPicker is a React Native form field that opens a searchable list: in a bottom sheet on phones, a dropdown under the field on the web, or a centered modal (presentation). Single or multiple selection, with groups, icons, descriptions, chips, a clear button and search on your server. It follows the iOS, Material or web design: a ✓ or radios, rounded or edge-to-edge rows.

import { SelectPicker } from '@elik56a/react-native-pickers';
Loading…
<SelectPicker
  label="Favourite fruit"
  placeholder="Pick one"
  options={fruits}
  value={fruit}
  onChange={setFruit}
/>
Always controlled

SelectPicker doesn't store the selection itself. Pass value and update it from onChange.

Try the props​

Platform
presentation
<SelectPicker
label="Fruit"
options={fruits}
value={value}
onChange={setValue}
searchable
/>

Options​

interface SelectOption<T> {
label: string; // row text, and what search matches first
value: T; // any type; compared with isEqual (default Object.is)
description?: string; // second line, also searchable
icon?: React.ReactNode; // leading element: flag, avatar, icon
group?: string; // section header
disabled?: boolean;
keywords?: string[]; // extra search terms, e.g. other languages or synonyms
}
Loading…
<SelectPicker
  label="Ship to"
  options={countries}
  value={country}
  onChange={setCountry}
  searchable
/>

Groups appear in the order their first option appears in options. Options without a group are listed together without a header.

Multiple selection​

multiple switches value to an array and onChange to (values, options). The sheet gets checkboxes and a Done button, and the field shows removable chips (tap a chip to remove it). maxSelected caps the selection; once reached, the remaining options are disabled.

Loading…
<SelectPicker
  multiple
  label="Skills (max 4)"
  placeholder="Add skills"
  options={skills}
  value={picked}
  onChange={setPicked}
  maxSelected={4}
  clearable
/>

Set chips={false} to show a comma-separated list instead of chips.

As a dropdown (the default on the web), the user types right in the field to filter the options; searchable={false} keeps the field read-only. In a sheet, the search field appears automatically when there are more than 8 options; force it with searchable / searchable={false}. The default filter:

  • matches label, description and keywords
  • is case-insensitive and accent-insensitive (creme finds Crème), and ignores Hebrew niqqud
  • splits the query on spaces and requires every word to match (new yo finds New York)

Provide filterOption for your own logic, for example matching only from the start of the label:

<SelectPicker
options={options}
value={value}
onChange={setValue}
filterOption={(option, query) => option.label.toLowerCase().startsWith(query.toLowerCase())}
/>

The default is exported as defaultFilter, so you can extend it:

import { defaultFilter } from '@elik56a/react-native-pickers';

filterOption={(o, q) => defaultFilter(o, q) || o.value === q.toUpperCase()}

Search on a server​

Pass loadOptions and the picker runs the search for you. It waits for a pause in typing (searchDebounce, 300 ms by default), shows the loader from the keystroke until the results arrive, and ignores answers to older queries. Before the user types, it lists options; with none, like here, it shows searchPrompt (default Type to search). A search with no results shows emptyText.

Loading…
<SelectPicker
  label="City"
  placeholder="Search a city"
  value={city}
  onChange={setCity}
  loadOptions={searchCities}
  searchPrompt="Type a city"
  emptyText="No city matches"
/>
  • Search field from the start, even with no options.
  • No double filtering: results are listed as loadOptions returns them. Pass filterOption to filter anyway.
  • The selected label stays in the field after a new search drops it from the results.
  • A failed request shows emptyText.

For a whole form built this way, see the flight search form recipe.

Searching yourself​

For full control (pagination, caching, your own state), use onSearchChange instead: it's called with the query once typing pauses, and you set options and loading. With options showing, loading puts the spinner under them, which suits onEndReached for loading the next page. See the Search a server recipe.

renderLoading replaces the spinner and text with your own element:

<SelectPicker
// …
loading={loading}
renderLoading={(theme) => <Text style={{ color: theme.colors.textSecondary }}>Asking the server…</Text>}
/>

Clearing​

clearable shows a × in the field when something is selected.

  • In multiple mode, × calls onChange([], []) and then onClear.
  • In single mode, × calls only onClear. Reset your state there:
Loading…
<SelectPicker
  label="Priority"
  clearable
  onClear={() => setV(null)}
  options={[
    { label: 'Low', value: 'a' },
    { label: 'Medium', value: 'b' },
    { label: 'High', value: 'c', description: 'Pages on-call' },
  ]}
  value={v}
  onChange={setV}
/>

Closing behaviour​

In single mode, the value changes as soon as an option is tapped and the sheet or dropdown closes a moment later, once the row's ripple has played (closeOnSelect, default true). With closeOnSelect={false} the sheet stays open and shows a Done button. Multiple mode always shows Done. onOpen and onClose fire when the sheet opens and closes.

Hover and ripple​

Rows tint while the pointer is over them and ripple from the touch point when picked; in single mode the list closes right after the ripple plays. The field tints on hover but doesn't ripple. Both colours come from the theme, so a brand tint is one prop:

Loading…
<SelectPicker
  label="Framework"
  placeholder="Choose a framework"
  options={frameworks}
  value={framework}
  onChange={setFramework}
  theme={{
    colors: {
      hover: 'rgba(200,70,13,0.08)',
      ripple: 'rgba(200,70,13,0.24)',
    },
  }}
/>

See Theming → Hover and ripple to change them everywhere or turn them off.

Custom trigger​

renderTrigger replaces the field. Call open() to show the sheet:

Loading…
<SelectPicker
  options={sorts}
  value={sort}
  onChange={setSort}
  sheetTitle="Sort by"
  renderTrigger={({ open, selected }) => (
    <Pressable
      onPress={open}
      style={{
        alignSelf: 'flex-start',
        paddingVertical: 8,
        paddingHorizontal: 14,
        borderRadius: 999,
        backgroundColor: '#EEF2FF',
      }}
    >
      <Text style={{ color: '#C8460D', fontWeight: '600' }}>
        ↕ {selected[0]?.label ?? 'Sort'}
      </Text>
    </Pressable>
  )}
/>

Custom chevron​

The field's chevron rotates when it opens and closes. renderChevron swaps the icon and keeps the rotation, so any icon works, an SVG included:

Loading…
<SelectPicker
  label="Size"
  options={sizes}
  value={size}
  onChange={setSize}
  renderChevron={({ theme }) => (
    <Text style={{ color: theme.colors.primary, fontSize: 16 }}>⌄</Text>
  )}
/>

Custom options​

renderOption replaces each row's content (taps, selection background and accessibility are still handled). renderGroupHeader replaces section headers.

Loading…
      <View
        style={{
          flexDirection: 'row',
          alignItems: 'center',
          gap: 12,
          flex: 1,
        }}
      >
        <View
          style={{
            width: 36,
            height: 36,
            borderRadius: 18,
            backgroundColor: p.color,
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text style={{ color: 'white', fontWeight: '700' }}>
            {p.label[0]}
          </Text>
        </View>
        <View style={{ flex: 1 }}>
          <Text
            style={{
              color: theme.colors.text,
              fontWeight: selected ? '700' : '500',
            }}
          >
            {p.label}
          </Text>
          <Text style={{ color: theme.colors.textSecondary, fontSize: 12 }}>
            {p.description}
          </Text>
        </View>
        {selected && <Text style={{ color: theme.colors.primary }}>●</Text>}
      </View>
    );
  }}
/>

Presentation​

presentation picks how the options open:

  • 'auto' (default): a dropdown on the web, a bottom sheet on native.
  • 'bottomSheet': always a bottom sheet.
  • 'modal': always a centered modal.
  • 'dropdown': a popover under the field, as wide as the field. It flips above the field when there's more room there, and a tap outside closes it.
Loading…
<SelectPicker
  label="Framework"
  value={value}
  onChange={setValue}
  options={['Expo', 'React Native CLI', 'Next.js', 'Vite'].map((label) => ({
    label,
    value: label,
  }))}
/>

Sheet options​

Customise the sheet through sheet, which takes PickerSheet props such as confirmLabel and sheetProps (passed through to BottomSheet):

<SelectPicker
options={options}
value={value}
onChange={setValue}
sheetTitle="Choose a city"
sheet={{ confirmLabel: 'Apply', sheetProps: { height: 0.7, enableSwipeToClose: false } }}
listHeight={400}
autoFocusSearch
/>

Expanding the sheet​

Long lists open at their content height and can grow to full screen: drag the header up on iOS and Android, or scroll the wheel up over the sheet on the web. The list grows with the sheet. Set the expanded height through sheet.sheetProps:

Loading…
<SelectPicker
  label="Country"
  options={countries}
  value={country}
  onChange={setCountry}
  presentation="bottomSheet"
  sheet={{ sheetProps: { expandedHeight: 0.9, draggable: true } }}
/>

expandable: false keeps the sheet at its content height; draggable: false turns off dragging on native. See BottomSheet for all the options.

Props​

Value​

PropTypeDefaultDescription
optionsReadonlyArray<SelectOption<T>>[]The options; with loadOptions, the ones shown before the user types.
multiplebooleanfalseMulti-select mode.
valueT | null · T[]—Selected value(s).
onChange(value: T, option: SelectOption<T>) => void · (values: T[], options: SelectOption<T>[]) => void—Single · multiple.
maxSelectednumber—Multiple only: maximum selected options.
isEqual(a: T, b: T) => booleanObject.isValue equality.

Field​

PropTypeDefaultDescription
labelstring—Label above the field; also the default sheet title.
placeholderstring'Select…'Shown when nothing is selected.
disabledbooleanfalseDisable the field.
clearablebooleanfalseShow a × button when something is selected.
onClear() => void—Called by ×. In single mode, reset value here.
chipsbooleantrueMultiple: show selections as removable chips.
renderChevron(info: { open, theme }) => ReactNode—Custom chevron icon (an SVG, an icon font…). It still rotates on open and close.
renderTrigger(info: { open, selected, disabled }) => ReactNode—Replace the field entirely.
styleStyleProp<ViewStyle>—Wrapper style (label + field).

Sheet & list​

PropTypeDefaultDescription
presentation'auto' | 'bottomSheet' | 'modal' | 'dropdown''auto'How the options open. auto: dropdown on the web, bottom sheet on native.
sheetTitlestringlabelSheet header title.
closeOnSelectbooleantrueSingle: close on tap. false shows a Done button.
sheetPartial<SheetChromeProps>—confirmLabel, cancelLabel, showCancel, title, sheetProps.
onOpen / onClose() => void—Sheet open / close.
searchablebooleandropdown: true · sheet: options.length > 8Dropdown: type in the field to filter, or in a search field atop the list with renderTrigger. Sheet: show the search field.
searchPlaceholderstring'Search'Search hint.
filterOption(option, query) => booleandefaultFilter; none with loadOptions / onSearchChangeCustom search.
loadOptions(query: string) => Promise<SelectOption<T>[]>—Search a server: loader, debounce and stale answers handled. An empty query shows options.
onSearchChange(query: string) => void—Called once typing pauses, to search yourself.
searchPromptstring'Type to search'Shown before the user types, while a searched list has no options.
searchDebouncenumber300ms after the last keystroke before loadOptions / onSearchChange.
onEndReached() => void—Called near the end of the list, to load more.
loadingbooleanfalseSpinner in place of an empty list, or under the options.
loadingTextstring'Loading…'Text next to the spinner.
renderLoading(theme) => ReactNode—Custom loader.
emptyTextstring'No results'Shown when nothing matches.
showCheckmarksbooleantrueCheck marks / checkboxes on rows.
renderOption(info: { option, selected, disabled, theme }) => ReactNode—Custom row content.
renderGroupHeader(group: string, theme) => ReactNode—Custom section header.
listHeightnumbergrows with contentFixed list height.
autoFocusSearchbooleanfalseFocus search when the sheet opens.
stylesSlotStyles<SelectSlot | SelectTriggerSlot>—Slots for both the field and the list.
themeDeepPartial<PickerTheme>—Theme override.
design'ios' | 'material' | 'web'from PickersProviderThe look to follow, over the provider's: iOS, Material or web.
testIDstring—Test ID on the list.

Slots​

Field (SelectTriggerSlot):

SlotElement
labelLabel text above the field
triggerThe field box
triggerTextSelected label(s)
placeholderPlaceholder text
chip / chipTextMulti-select chips
chevronThe drawn chevron (a bordered View: borderColor sets its color, width / height its size)
clear× button text

List: see SelectList slots.

Accessibility​

The field is a button labelled with label (or the placeholder), with the current selection as its hint and an expanded state. Options are buttons (single) or checkboxes (multiple) with selected / checked / disabled states, and chips announce Remove {label}.