Select
Free · MITReact Native · ExpoA field that opens a bottom sheet list with search, sections and checkmarks that draw themselves in. Single mode closes on a pick; multiple mode shows the picks as chips in the field that grow in and shrink away live behind the sheet.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/select.jsonThen run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/select.
Usage
import { useState } from "react";
import { Select } from "@/components/goodui-native/select/select";
export function CountryField() {
const [country, setCountry] = useState<string[]>([]);
return (
<Select
label="Country"
placeholder="Choose a country"
value={country}
onValueChange={setCountry}
options={[
{ value: "de", label: "Germany", section: "Europe", keywords: ["DE", "Deutschland"] },
{ value: "fr", label: "France", section: "Europe" },
{ value: "tr", label: "Türkiye", section: "Europe" },
{ value: "us", label: "United States", section: "Americas", keywords: ["USA"] },
{ value: "br", label: "Brazil", section: "Americas" },
]}
/>
);
}When to use
- Choosing one or several values from a list too long to show inline: country, category, team members, timezone.
- Lists that need search, sections or a second line of detail per option.
- Two to five short options: use segmented-control or a ChipGroup from chip.
- An ordered range people scroll through (hours, quantity): use wheel-picker.
- Actions rather than values: use action-sheet or context-menu.
Examples
Multiple with a limit and chips in the field
<Select
mode="multiple"
label="Assignees"
max={3}
value={people}
onValueChange={setPeople}
options={team.map((m) => ({ value: m.id, label: m.name, description: m.role, leading: <Avatar uri={m.photo} size={28} /> }))}
/>Error and helper text
<Select label="Plan" options={plans} value={plan} onValueChange={setPlan} error={plan.length ? undefined : "Choose a plan to continue"} />API reference
| Prop | Type | Default |
|---|---|---|
| options* The choices. Give them a `section` to group the list. | SelectOption<T>[] | — |
| mode `single` closes the sheet on a pick; `multiple` toggles rows and closes with Done. | "single" | "multiple" | "single" |
| value Selected values (controlled). Always an array, also in single mode. | T[] | — |
| defaultValue Starting selection when uncontrolled. | T[] | — |
| onValueChange Called with the new selection after every pick or removal. | (value: T[]) => void | — |
| label Field label above the value. | string | — |
| placeholder Text in the field when nothing is selected. Same as `labels.placeholder`. | string | — |
| title Sheet title. Defaults to the label. | string | — |
| description Supporting line under the sheet title. | string | — |
| searchable Show a search field in the sheet. Defaults to on when there are more than 8 options. | boolean | — |
| searchPlaceholder Placeholder of the search field. Same as `labels.searchPlaceholder`. | string | — |
| max Multiple mode: the most values that can be selected. Past it a pick is refused with a warning haptic. | number | — |
| maxChips Multiple mode: how many chips the field shows before "+n". Default 3. | number | 3 |
| variant `outline` has a border, `filled` a quiet fill. | "outline" | "filled" | "outline" |
| error Error text under the field; also turns the border red. | string | — |
| helperText Help text under the field when there is no error. | string | — |
| disabled Dims the control and ignores presses. | boolean | false |
| doneLabel Label of the button that closes the sheet in multiple mode. Same as `labels.done`. | string | — |
| emptyLabel Shown in the sheet when a search matches nothing. Same as `labels.empty`. | string | — |
| open Controlled sheet state. Usually leave it to the field. | boolean | — |
| onOpenChange Called when the sheet opens or closes. | (open: boolean) => void | — |
| haptic `false` silences the select and warning haptics. | boolean | true |
| accessibilityLabel Screen reader name of the field. Defaults to the label. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<SelectLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- The chevron turns 180° on the snappy spring as the sheet opens and back as it closes.
- A picked row's check draws itself in; in single mode the sheet waits 220 ms so the check finishes before it leaves.
- Multiple mode: chips added after the field first rendered grow in from 60 % on the morph spring; removed chips shrink away (chip's own removal).
- The sheet slides with bottom-sheet's springs.
- Reduced motion: the chevron turns with a short timing, checks appear without drawing, chips appear without growing, and single mode closes after 60 ms.
- Haptic: select when a value is picked or toggled
- Haptic: warning when max is reached
- Haptic: chip removal plays the chip's tap
- Haptic: haptic={false} silences the select and warning
- Filtering runs on each keystroke over labels, descriptions and keywords, accent and case insensitive. Rows are plain views in the sheet's scroll view, so keep lists to a few hundred options.
Accessibility
- The field is a button whose label starts with the visible label, then the value ("Country, Germany"), with expanded and disabled in accessibilityState and aria-expanded and aria-haspopup on the web.
- Rows are radios in a radiogroup (single) or checkboxes (multiple), with checked and disabled states; a description is read after the label.
- Chips in the field have their own "Remove <label>" buttons, so values can be removed without opening the sheet; removal is announced by the chip.
- Hitting max is refused with a warning haptic and announced ("Choose up to 3").
- The sheet is the kit's bottom-sheet: modal, closable with back, the backdrop or a drag, and its grabber is adjustable.
- Rows are at least 52 pt tall; the field at least 48 pt.
Notes for AI
- value is always an array, also in single mode: value={[country]} onValueChange={([c]) => setCountry(c)}.
- Search appears by default above 8 options; force it with searchable.
- Group rows with option.section; sections keep the order in which they first appear in options.
- Use keywords for aliases and codes that should match a search but not show.
- Do not wrap Select in your own Pressable or sheet; it owns both.
Related
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.
Filter, choice and input chips: selecting one fills it with the accent while a check draws itself in, removing one shrinks it away, and a ChipGroup in single mode moves one elastic highlight from chip to chip.
An iOS-style search field: focusing it shrinks the field while a Cancel button springs in beside it and scope chips cascade in below. Clear button, loading spinner and debounced queries built in.
iOS-style wheel: rows sit on a cylinder in 3D perspective, follow the finger 1:1, keep the fling's momentum and settle exactly on a row with a select tick for every row that passes. Compose several into one picker with WheelPickerGroup. On the web it scrolls with the mouse wheel and drags with the mouse.