# Select

> A React select with search, groups, icons, virtualized rows and a hidden native select for forms.

- Type: React component (Inputs)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add select`
- Docs: https://wingo-ui.com/components/select

## Interaction

Click the field (or tap it) to open the list under it, growing out of the field; click an option to pick it, a check draws itself next to it and the list closes. Clicking the field again, clicking outside, Escape or Tab closes it without changing anything. Above 8 options (or with search="always") a search row sits on top and takes focus: typing filters as you go, case and diacritics never matter ("canon" finds Cañon City, state codes like NY work as keywords), matched letters turn bold and groups without matches disappear. Keyboard: Tab reaches the field and a click on its label only focuses it. While closed, Enter, Space, ArrowDown or ArrowUp open it on the chosen option (ArrowUp on the last one when nothing is chosen), Home and End open it on the first and last option, and typing letters picks the next option that starts with them without opening, like a native select (the same letter again cycles). While open, ArrowDown and ArrowUp move and wrap, PageUp and PageDown jump a screen, Home and End go to the ends, Enter picks and closes, Escape or Alt+ArrowUp close and keep focus on the field. With clearable an x appears while a value is set; it clears it and focus goes back to the field. With loading a spinner replaces the chevron and the list shows a loading row. Lists above the virtual threshold render only the rows in view, so thousands of cities scroll at 60fps. On a phone the field opens a bottom sheet titled with the label, with 48px rows (a tap picks and closes), the chosen row scrolled into view, and the search row under the title (it waits for a tap so the keyboard does not cover the list, unless search is always). With name, a hidden native select submits the value, blocks an empty required field and lets browser autofill fill the state. With reduced motion the list fades instead of scaling and the check appears without drawing.

## Installation

```bash
npx wingo-ui@latest add select
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/select.json`

Files:

- `components/ui/select.tsx`

npm dependencies: `npm i @radix-ui/react-popover @radix-ui/react-slot motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { MapPin } from "lucide-react"
import { Select, type SelectOption } from "@/components/ui/select"

const STATES: SelectOption[] = [
  { value: "CA", label: "California", keywords: ["CA", "Los Angeles"] },
  { value: "NY", label: "New York", keywords: ["NY", "New York City"] },
  { value: "OR", label: "Oregon", keywords: ["OR", "Portland"] },
  { value: "TX", label: "Texas", keywords: ["TX", "Austin"] },
]

export function ShippingState() {
  const [state, setState] = useState("")
  return (
    <Select
      label="State"
      placeholder="Choose the state"
      searchPlaceholder="Search a state or its code…"
      emptyText="No state found"
      options={STATES}
      value={state}
      onValueChange={setState}
      leftIcon={<MapPin />}
      clearable
      required
      name="state"
      autoComplete="address-level1"
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `SelectItems` |  | Options and groups: { value, label, description?, icon?, badge?, disabled?, keywords?, textValue? } or { label, items }. keywords add search terms, textValue names a non-text label. |
| `value?` | `string` |  | The chosen option, controlled ("" is nothing). A value not among the options shows as text and highlights nothing. |
| `defaultValue?` | `string` | `""` | The starting value while uncontrolled. A form reset returns to it. |
| `onValueChange?` | `(value: string) => void` |  | Called with the new value after a pick, a typeahead jump, a clear or browser autofill. |
| `label?` | `ReactNode` | `""` | The field label. A click focuses the trigger (never opens it); plain text also titles the phone sheet. |
| `description?` | `ReactNode` | `""` | Helper text under the field, linked with aria-describedby. |
| `error?` | `ReactNode` | `""` | Replaces the description with an animated error and marks the field invalid. |
| `required?` | `boolean` | `false` | The red mark after the label, aria-required on the trigger and required on the hidden native select. |
| `invalid?` | `boolean` | `false` | The invalid look and aria-invalid without a message. |
| `disabled?` | `boolean` | `false` | Dims the field, never opens and is not submitted. |
| `readOnly?` | `boolean` | `false` | The value stays visible and the field focusable, but it never opens, typeahead does nothing and there is no clear button. |
| `placeholder?` | `string` | `"Select an option"` | Shown in the trigger while nothing is chosen. |
| `search?` | `"auto" \| "always" \| "never"` | `"auto"` | The search row on top of the list: auto shows it above 8 options. |
| `searchPlaceholder?` | `string` | `"Search…"` | The search row's placeholder. |
| `emptyText?` | `string` | `"No results"` | Shown when the search matches nothing (or there are no options). |
| `clearable?` | `boolean` | `false` | An x in the trigger while a value is set; it clears the value and focus returns to the trigger. |
| `clearLabel?` | `string` | `"Clear"` | The clear button's accessible name and tooltip. |
| `loading?` | `boolean` | `false` | Options are on their way: a spinner replaces the chevron and the list shows a loading row until options arrive. |
| `loadingText?` | `string` | `"Loading…"` | The loading row's text and the spinner's accessible name. |
| `leftIcon?` | `ReactNode` |  | A leading icon, shown while the chosen option has no icon of its own (or showSelectedIcon is off). |
| `showSelectedIcon?` | `boolean` | `true` | The chosen option's icon (a flag, a swatch, a plate code) in the trigger. |
| `renderValue?` | `(option: SelectOption) => ReactNode` |  | Custom trigger content for the chosen option; the icon and the placeholder stay. |
| `highlightMatches?` | `boolean` | `true` | Bold matched letters while searching. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | 32 / 40 / 48px (md grows to 44px on touch), 16px text on phones. |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The field look, the same as every other field. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius, scaled per size like the button; ignored by underline. |
| `name?` | `string` | `""` | Renders a hidden native select with this name, so FormData, required validation, form reset and autofill work. |
| `autoComplete?` | `string` | `""` | For the hidden native select, so browser autofill can fill it ("address-level1" for a county). |
| `id?` | `string` |  | The trigger's id (useId() by default); the label points at it. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel. |
| `defaultOpen?` | `boolean` | `false` | Starts with the list (or the sheet) open. Pass open and onOpenChange to control it. |
| `open?` | `boolean` |  | Whether the list is open, controlled. It never opens while disabled or readOnly. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the list opens or closes. |
| `responsive?` | `boolean` | `true` | A bottom sheet with 48px rows below 768px instead of the popover. |
| `sheetTitle?` | `string` | `""` | The phone sheet's title. Empty uses the label's text, else "Select an option". |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Where the list opens; it flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | How the list lines up with the field. |
| `contentWidth?` | `"trigger" \| "auto" \| number` | `"trigger"` | The list's width: the field's, its content's (at least the field's), or px. |
| `maxHeight?` | `number` | `360` | The list's maximum height in px, capped by the room on screen. |
| `virtualThreshold?` | `number` | `100` | Above this many visible options only the rows in view render. |
| `renderOption?` | `(option, state: { selected, highlighted, disabled, ranges }) => ReactNode` |  | Replaces a row's content; the row element, its aria and its classes stay. |
| `container?` | `HTMLElement \| null` |  | Where the list or the sheet is portaled (document.body by default). |
| `labels?` | `Partial<{ results: (count: number) => string; clearSearch: string; retry: string }>` |  | Screen reader and search strings: the result count announced after a search ("12 results"), the search clear button. |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "counter" \| "box" \| "trigger" \| "value" \| "placeholder" \| "icon" \| "chevron" \| "clear" \| "content" \| "search" \| "list" \| "item" \| "group" \| "empty", string>>` |  | Extra classes for the inner parts. box is the field box, trigger the button inside it, content the popover (or the sheet). |
| `className?` | `string` |  | Merged onto the Field root when there is a label, description or error, otherwise onto the box. |
| `ref?` | `RefObject<HTMLButtonElement \| null> or (node) => void` |  | The trigger button; other props (onBlur, aria-*, data-*, form) go there too. |

## Parts

### SelectRoot (component)

Owns the listbox engine and the open state and renders no DOM. Reads flags and ids from an enclosing Field. Props: { options, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, search, responsive, loading, virtualThreshold, filter, fuzzy, disabled, readOnly, required, invalid, size, id, name, autoComplete, form, children }.

### SelectTrigger (component)

The field box with the trigger button, clear button and chevron (and the hidden native select). asChild turns your own element into the trigger (a ghost sort button, a flag button). Props: { asChild, variant, radius, size, clearable, clearLabel, leftIcon, showSelectedIcon, loadingText, classNames, ...button }.

### SelectValue (component)

The chosen option's label (or renderValue), else the placeholder. Props: { placeholder, renderValue, placeholderClassName, ...span }.

### SelectAnchor (component)

What the list aligns to when the trigger is small, e.g. the whole phone field around a flag button. Props: Slot props.

### SelectContent (component)

The popover list on desktop, the bottom sheet on phones. Props: { contentWidth, maxHeight, side, align, sideOffset, container, sheetTitle, searchPlaceholder, emptyText, loadingText, highlightMatches, renderOption, labels, className, classNames }.

## Examples

- **Groups, icons and counts**: Groups with sticky labels, descriptions and counts. Search appears on its own above 8 options; a disabled row is skipped.
- **Virtualized**: About 275 cities render virtually. Diacritics never matter: “canon” finds Cañon City, “oregon” the state.
- **Clearable and required**: An x clears the value and returns focus to the trigger; a required field shows its error until something is picked.
- **Sizes, variants and radius**: 32, 40 and 48px, the four field looks and the radius scale, identical to every other field.
- **Custom value**: renderValue adds the price next to the name; the option's swatch becomes the trigger icon.
- **Parts: a sort button**: SelectRoot with SelectTrigger asChild turns a ghost button into the trigger; contentWidth auto sizes the list to its rows.
- **Native form**: name renders a hidden native select: FormData, required validation, reset and autofill work like a real one.

## See also

- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))
- [Combobox](https://wingo-ui.com/components/combobox) ([Markdown](https://wingo-ui.com/components/combobox.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Radio Group](https://wingo-ui.com/components/radio-group) ([Markdown](https://wingo-ui.com/components/radio-group.md))
- [Segmented Control](https://wingo-ui.com/components/segmented-control) ([Markdown](https://wingo-ui.com/components/segmented-control.md))

---

Canonical: https://wingo-ui.com/components/select
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
