# Select & MultiSelect (React Native)

> Pick one or many from a list, searchable and virtualised. A bottom sheet on phones, a popover on tablets.

GramproKit for React Native · Pickers · Beta · Source: https://kit.gramproindia.com/native/docs/select

> **Beta:** React Native support is in beta. APIs may change or break between prereleases (npm tag `native`).

_Interactive demo:_ [open the live example](https://kit.gramproindia.com/native/docs/select). The example's source:

```tsx
import { useMemo, useState } from "react";
import { Text } from "react-native";
import { MultiSelect, Select, type ComboboxOption } from "@/component-lib/combobox";
import { useKitTheme } from "@/component-lib/shared/native";
import { Section } from "./layout"; // Section and Row: page layout for the demo

const STATES: ComboboxOption[] = [
  ["KL", "Kerala", "South"],
  ["TN", "Tamil Nadu", "South"],
  ["KA", "Karnataka", "South"],
  ["AP", "Andhra Pradesh", "South"],
  ["TG", "Telangana", "South"],
  ["MH", "Maharashtra", "West"],
  ["GJ", "Gujarat", "West"],
  ["GA", "Goa", "West"],
  ["RJ", "Rajasthan", "North"],
  ["PB", "Punjab", "North"],
  ["UP", "Uttar Pradesh", "North"],
  ["DL", "Delhi", "North"],
  ["WB", "West Bengal", "East"],
  ["OD", "Odisha", "East"],
  ["AS", "Assam", "North-east"],
  ["MN", "Manipur", "North-east"],
].map(([value, label, group]) => ({ value, label, group, keywords: [value] }));

const SKILLS: ComboboxOption[] = [
  "React", "React Native", "TypeScript", "Node.js", "GraphQL", "PostgreSQL", "Figma", "Kotlin", "Swift", "Go",
].map((label) => ({ value: label.toLowerCase(), label }));

export function ComboboxDemo() {
  const { tokens } = useKitTheme();
  const [state, setState] = useState<string | null>("KL");
  const [skills, setSkills] = useState<string[]>(["react", "typescript"]);
  const [extra, setExtra] = useState<ComboboxOption[]>([]);
  const [account, setAccount] = useState<string | null>(null);

  const many = useMemo<ComboboxOption[]>(
    () =>
      Array.from({ length: 500 }, (_, i) => ({
        value: `acc-${i + 1}`,
        label: `Account ${String(i + 1).padStart(3, "0")}`,
        description: i % 7 === 0 ? "Dormant since 2024" : `Ledger ${(i % 12) + 1}`,
        disabled: i % 41 === 40,
      })),
    [],
  );

  return (
    <>
      <Section title="Select · grouped, searchable">
        <Select
          label="State"
          options={STATES}
          value={state}
          onChange={(value) => setState(value)}
          description="Search by name or code (e.g. “KL”)."
        />
        <Text style={{ color: tokens.muted }}>Value: {String(state)}</Text>
      </Section>

      <Section title="MultiSelect · tags, max, create">
        <MultiSelect
          label="Skills"
          options={[...SKILLS, ...extra]}
          value={skills}
          onChange={setSkills}
          max={5}
          maxVisibleTags={3}
          allowCreate
          onCreate={(label) => {
            const option = { value: label.toLowerCase(), label };
            setExtra((list) => [...list, option]);
            setSkills((list) => (list.length < 5 ? [...list, option.value] : list));
          }}
        />
      </Section>

      <Section title="Select · 500 options, virtualised">
        <Select
          label="Ledger account"
          options={many}
          value={account}
          onChange={setAccount}
          placeholder="Pick an account"
        />
      </Section>

      <Section title="Select · error and disabled">
        <Select label="Branch" options={STATES} required error="Choose a branch to continue." />
        <Select label="Region" options={STATES} defaultValue="TN" disabled />
        <MultiSelect label="Tags" options={SKILLS} searchable={false} size="sm" placeholder="No search box" />
      </Section>
    </>
  );
}
```

## Install

```bash
npx @grampro/kit@native add Select
```

## Usage

```tsx
import { Select, MultiSelect } from "@/component-lib/combobox";

<Select
  label="State"
  options={[
    { value: "KL", label: "Kerala" },
    { value: "TN", label: "Tamil Nadu" },
  ]}
  onChange={setState}
/>
```

## Props

### SelectProps

| Prop | Type | Description |
|---|---|---|
| `value` | `V \| null` |   |
| `defaultValue` | `V \| null` |   |
| `onChange` | `(value: NoInfer<V> \| null, option: ComboboxOption<NoInfer<V>> \| null) => void` | The chosen value and its option; both `null` once cleared. |
| `closeOnSelect` | `boolean` | Close the list after choosing. Default true. |
| `options` (required) | `readonly ComboboxOption<V>[]` |   |
| `mode` | `"client" \| "server"` | `client` filters `options` locally; `server` expects `options` to be the current results. |
| `loading` | `boolean` | Shows a loading row; keeps current options visible. |
| `onSearchChange` | `(search: string) => void` | Called with the search text (debounced in server mode), and on open. |
| `searchDebounce` | `number` | Debounce for `onSearchChange` in server mode. Default 250 ms. |
| `searchable` | `boolean` | Show the search box. Default true. |
| `hasMore` | `boolean` | More results are available; the list calls `onLoadMore` near its end. |
| `onLoadMore` | `() => void` |   |
| `filterFn` | `(option: ComboboxOption<NoInfer<V>>, search: string) => boolean` |   |
| `renderOption` | `(option: ComboboxOption<NoInfer<V>>, state: OptionState) => ReactNode` |   |
| `allowCreate` | `boolean` | Offers "Create …" when the search matches no option. |
| `onCreate` | `(label: string) => void` |   |
| `label` | `ReactNode` |   |
| `placeholder` | `string` |   |
| `description` | `ReactNode` |   |
| `error` | `ReactNode` |   |
| `required` | `boolean` |   |
| `disabled` | `boolean` |   |
| `clearable` | `boolean` | Show the clear button. Default true. |
| `size` | `ComboboxSize` | Default `md`. |
| `maxHeight` | `number` | Max height of the list in the tablet popover, in dp. Default 280. A phone sheet uses the screen. |
| `presentation` | `"auto" \| "sheet" \| "popover"` | `auto` (default): bottom sheet on a phone, popover on a tablet. |
| `emptyMessage` | `ReactNode` | Shown when nothing matches the search. Defaults to the locale text. |
| `style` | `Styleable<ViewStyle>` |   |
| `styles` | `ComboboxStyles` |   |
| `localeText` | `Partial<ComboboxLocaleText>` |   |
| `onOpenChange` | `(open: boolean) => void` |   |

### MultiSelectProps

| Prop | Type | Description |
|---|---|---|
| `value` | `V[]` |   |
| `defaultValue` | `V[]` |   |
| `onChange` | `(values: NoInfer<V>[], options: ComboboxOption<NoInfer<V>>[]) => void` |   |
| `max` | `number` | Maximum number of selections. |
| `maxVisibleTags` | `number` | Tags shown before collapsing into "+N more". Default 3. |
| `showSelectAll` | `boolean` | Show "Select all" / "Clear all" under the list. Default true. |
| `closeOnSelect` | `boolean` | Close the list after each choice. Default false. |
| `options` (required) | `readonly ComboboxOption<V>[]` |   |
| `mode` | `"client" \| "server"` | `client` filters `options` locally; `server` expects `options` to be the current results. |
| `loading` | `boolean` | Shows a loading row; keeps current options visible. |
| `onSearchChange` | `(search: string) => void` | Called with the search text (debounced in server mode), and on open. |
| `searchDebounce` | `number` | Debounce for `onSearchChange` in server mode. Default 250 ms. |
| `searchable` | `boolean` | Show the search box. Default true. |
| `hasMore` | `boolean` | More results are available; the list calls `onLoadMore` near its end. |
| `onLoadMore` | `() => void` |   |
| `filterFn` | `(option: ComboboxOption<NoInfer<V>>, search: string) => boolean` |   |
| `renderOption` | `(option: ComboboxOption<NoInfer<V>>, state: OptionState) => ReactNode` |   |
| `allowCreate` | `boolean` | Offers "Create …" when the search matches no option. |
| `onCreate` | `(label: string) => void` |   |
| `label` | `ReactNode` |   |
| `placeholder` | `string` |   |
| `description` | `ReactNode` |   |
| `error` | `ReactNode` |   |
| `required` | `boolean` |   |
| `disabled` | `boolean` |   |
| `clearable` | `boolean` | Show the clear button. Default true. |
| `size` | `ComboboxSize` | Default `md`. |
| `maxHeight` | `number` | Max height of the list in the tablet popover, in dp. Default 280. A phone sheet uses the screen. |
| `presentation` | `"auto" \| "sheet" \| "popover"` | `auto` (default): bottom sheet on a phone, popover on a tablet. |
| `emptyMessage` | `ReactNode` | Shown when nothing matches the search. Defaults to the locale text. |
| `style` | `Styleable<ViewStyle>` |   |
| `styles` | `ComboboxStyles` |   |
| `localeText` | `Partial<ComboboxLocaleText>` |   |
| `onOpenChange` | `(open: boolean) => void` |   |

## Native and web

- `presentation`: `"auto"` (sheet below 600dp, popover above), `"sheet"` or `"popover"`.
- Change events are `onChange`, as on the web. No keyboard navigation or type-ahead on touch.
- Web version: [Select & MultiSelect on the web](https://kit.gramproindia.com/combobox).
