Skip to main content
Version: Next ๐Ÿšง

SelectPicker

SelectPicker is a React Native form field that opens a bottom sheet with a searchable list: a dropdown / select that feels native on phones. Single or multiple selection, with groups, icons, descriptions, chips and a clear button.

import { SelectPicker } from '@elik56a/react-native-pickers';
Result
Loading...
Live Editor
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';

const fruits = ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange', 'Peach', 'Pear', 'Plum', 'Strawberry', 'Watermelon'].map((f) => ({
  label: f,
  value: f.toLowerCase(),
}));

export default function Example() {
  const [fruit, setFruit] = useState<string | null>(null);
  return <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โ€‹

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
}
Result
Loading...
Live Editor
import { useState } from 'react';
import { Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';

const countries = [
  { label: 'Israel', value: 'IL', icon: <Text>๐Ÿ‡ฎ๐Ÿ‡ฑ</Text>, group: 'Middle East', keywords: ['ื™ืฉืจืืœ'] },
  { label: 'Jordan', value: 'JO', icon: <Text>๐Ÿ‡ฏ๐Ÿ‡ด</Text>, group: 'Middle East' },
  { label: 'France', value: 'FR', icon: <Text>๐Ÿ‡ซ๐Ÿ‡ท</Text>, group: 'Europe', description: 'Rรฉpublique franรงaise' },
  { label: 'Germany', value: 'DE', icon: <Text>๐Ÿ‡ฉ๐Ÿ‡ช</Text>, group: 'Europe', keywords: ['deutschland'] },
  { label: 'Spain', value: 'ES', icon: <Text>๐Ÿ‡ช๐Ÿ‡ธ</Text>, group: 'Europe', disabled: true, description: 'Not shipping yet' },
  { label: 'Japan', value: 'JP', icon: <Text>๐Ÿ‡ฏ๐Ÿ‡ต</Text>, group: 'Asia', keywords: ['nippon'] },
];

export default function Example() {
  const [country, setCountry] = useState<string | null>('FR');
  return <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.

Result
Loading...
Live Editor
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';

const skills = ['React', 'React Native', 'TypeScript', 'Expo', 'Node.js', 'GraphQL', 'Swift', 'Kotlin'].map((s) => ({ label: s, value: s }));

export default function Example() {
  const [picked, setPicked] = useState<string[]>(['React Native', 'TypeScript']);
  return (
    <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()}

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:
Result
Loading...
Live Editor
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';

export default function Example() {
  const [v, setV] = useState<string | null>('b');
  return (
    <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:

Result
Loading...
Live Editor
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';

const frameworks = ['Expo', 'React Native CLI', 'React Native Web', 'Next.js', 'Remix', 'Vite'].map((f) => ({ label: f, value: f }));

export default function Example() {
  const [framework, setFramework] = useState<string | null>(null);
  return (
    <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:

Result
Loading...
Live Editor
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';

const sorts = [
  { label: 'Most relevant', value: 'relevance' },
  { label: 'Newest first', value: 'newest' },
  { label: 'Price: low to high', value: 'price_asc' },
  { label: 'Price: high to low', value: 'price_desc' },
];

export default function Example() {
  const [sort, setSort] = useState<string | null>('relevance');
  return (
    <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:

Result
Loading...
Live Editor
import { useState } from 'react';
import { Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';

const sizes = ['Small', 'Medium', 'Large'].map((s) => ({ label: s, value: s }));

export default function Example() {
  const [size, setSize] = useState<string | null>(null);
  return (
    <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.

Result
Loading...
Live Editor
import { useState } from 'react';
import { View, Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';

const people = [
  { label: 'Ada Lovelace', value: 'ada', description: 'Mathematician', color: '#F59E0B' },
  { label: 'Alan Turing', value: 'alan', description: 'Computer scientist', color: '#10B981' },
  { label: 'Grace Hopper', value: 'grace', description: 'Rear admiral', color: '#6366F1' },
];

export default function Example() {
  const [who, setWho] = useState<string | null>(null);
  return (
    <SelectPicker
      label="Assignee"
      options={people}
      value={who}
      onChange={setWho}
      renderOption={({ option, selected, theme }) => {
        const p = people.find((x) => x.value === option.value)!;
        return (
          <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.
Result
Loading...
Live Editor
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';

export default function Example() {
  const [value, setValue] = useState(null);
  return (
    <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:

Result
Loading...
Live Editor
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';

const countries = [
  'Argentina', 'Australia', 'Austria', 'Belgium', 'Brazil', 'Canada', 'Chile', 'China', 'Denmark', 'Egypt',
  'Finland', 'France', 'Germany', 'Greece', 'India', 'Ireland', 'Israel', 'Italy', 'Japan', 'Mexico',
  'Netherlands', 'Norway', 'Poland', 'Portugal', 'Spain', 'Sweden', 'Switzerland', 'Turkey', 'United Kingdom', 'United States',
].map((c) => ({ label: c, value: c }));

export default function Example() {
  const [country, setCountry] = useState<string | null>(null);
  return (
    <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>>requiredThe options.
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. Sheet: show the search field.
searchPlaceholderstring'Search'Search hint.
filterOption(option, query) => booleandefaultFilterCustom search.
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.
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}.