Select

Free · MITReact Native · Expo

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

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/select.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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.
number3
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.
booleanfalse
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.
booleantrue
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.