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';
<SelectPicker label="Favourite fruit" placeholder="Pick one" options={fruits} value={fruit} onChange={setFruit} />
SelectPicker doesn't store the selection itself. Pass value and update it from onChange.
Try the props
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
}
<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.
<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.
Search
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,descriptionandkeywords - is case-insensitive and accent-insensitive (
cremefindsCrème), and ignores Hebrew niqqud - splits the query on spaces and requires every word to match (
new yofindsNew 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.
<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
loadOptionsreturns them. PassfilterOptionto 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 thenonClear. - In single mode, × calls only
onClear. Reset your state there:
<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:
<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:
<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:
<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.
<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.
<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:
<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
| Prop | Type | Default | Description |
|---|---|---|---|
options | ReadonlyArray<SelectOption<T>> | [] | The options; with loadOptions, the ones shown before the user types. |
multiple | boolean | false | Multi-select mode. |
value | T | null · T[] | — | Selected value(s). |
onChange | (value: T, option: SelectOption<T>) => void · (values: T[], options: SelectOption<T>[]) => void | — | Single · multiple. |
maxSelected | number | — | Multiple only: maximum selected options. |
isEqual | (a: T, b: T) => boolean | Object.is | Value equality. |
Field
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Label above the field; also the default sheet title. |
placeholder | string | 'Select…' | Shown when nothing is selected. |
disabled | boolean | false | Disable the field. |
clearable | boolean | false | Show a × button when something is selected. |
onClear | () => void | — | Called by ×. In single mode, reset value here. |
chips | boolean | true | Multiple: 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. |
style | StyleProp<ViewStyle> | — | Wrapper style (label + field). |
Sheet & list
| Prop | Type | Default | Description |
|---|---|---|---|
presentation | 'auto' | 'bottomSheet' | 'modal' | 'dropdown' | 'auto' | How the options open. auto: dropdown on the web, bottom sheet on native. |
sheetTitle | string | label | Sheet header title. |
closeOnSelect | boolean | true | Single: close on tap. false shows a Done button. |
sheet | Partial<SheetChromeProps> | — | confirmLabel, cancelLabel, showCancel, title, sheetProps. |
onOpen / onClose | () => void | — | Sheet open / close. |
searchable | boolean | dropdown: true · sheet: options.length > 8 | Dropdown: type in the field to filter, or in a search field atop the list with renderTrigger. Sheet: show the search field. |
searchPlaceholder | string | 'Search' | Search hint. |
filterOption | (option, query) => boolean | defaultFilter; none with loadOptions / onSearchChange | Custom 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. |
searchPrompt | string | 'Type to search' | Shown before the user types, while a searched list has no options. |
searchDebounce | number | 300 | ms after the last keystroke before loadOptions / onSearchChange. |
onEndReached | () => void | — | Called near the end of the list, to load more. |
loading | boolean | false | Spinner in place of an empty list, or under the options. |
loadingText | string | 'Loading…' | Text next to the spinner. |
renderLoading | (theme) => ReactNode | — | Custom loader. |
emptyText | string | 'No results' | Shown when nothing matches. |
showCheckmarks | boolean | true | Check marks / checkboxes on rows. |
renderOption | (info: { option, selected, disabled, theme }) => ReactNode | — | Custom row content. |
renderGroupHeader | (group: string, theme) => ReactNode | — | Custom section header. |
listHeight | number | grows with content | Fixed list height. |
autoFocusSearch | boolean | false | Focus search when the sheet opens. |
styles | SlotStyles<SelectSlot | SelectTriggerSlot> | — | Slots for both the field and the list. |
theme | DeepPartial<PickerTheme> | — | Theme override. |
design | 'ios' | 'material' | 'web' | from PickersProvider | The look to follow, over the provider's: iOS, Material or web. |
testID | string | — | Test ID on the list. |
Slots
Field (SelectTriggerSlot):
| Slot | Element |
|---|---|
label | Label text above the field |
trigger | The field box |
triggerText | Selected label(s) |
placeholder | Placeholder text |
chip / chipText | Multi-select chips |
chevron | The 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}.
Related
- SelectList: the list without the field and sheet
- Picker fields: the same field for dates, times, wheels and emoji
CountryPicker: this field with about 250 countries built in, and the country and phone number recipe- Tag picker recipe
- Search a server recipe: loading, debounced search and load-more
- Flight search form recipe: a full form with airports searched on a server