---
name: select
type: core
domain: forms
requires: [loke-ui]
description: >
  Select listbox with SelectTrigger, SelectValue, SelectPortal, SelectContent,
  SelectViewport, SelectGroup, SelectLabel, SelectItem + SelectItemText (required),
  SelectItemIndicator, SelectScrollUpButton, SelectScrollDownButton, SelectSeparator,
  SelectIcon, SelectArrow. Two positioning modes: item-aligned (default) and popper.
  Empty string value throws. All items must be in tree at open time — use Popover+Command
  for async/searchable lists.
---

# Select

`@loke/ui/select` — accessible listbox-style select built on a custom `role="combobox"` trigger. A hidden native `<select>` handles form participation and autofill.

See [`references/select-components.md`](references/select-components.md) for the full sub-component prop reference.

## Setup

```tsx
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectItemText,
  SelectLabel,
  SelectPortal,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectTrigger,
  SelectValue,
  SelectViewport,
} from "@loke/ui/select";

function CountrySelect() {
  return (
    <Select name="country" defaultValue="au">
      <SelectTrigger style={{ width: 200 }}>
        <SelectValue placeholder="Select a country" />
      </SelectTrigger>

      <SelectPortal>
        <SelectContent>
          <SelectScrollUpButton />
          <SelectViewport>
            <SelectGroup>
              <SelectLabel>Asia Pacific</SelectLabel>
              <SelectItem value="au">
                <SelectItemText>Australia</SelectItemText>
              </SelectItem>
              <SelectItem value="nz">
                <SelectItemText>New Zealand</SelectItemText>
              </SelectItem>
            </SelectGroup>
          </SelectViewport>
          <SelectScrollDownButton />
        </SelectContent>
      </SelectPortal>
    </Select>
  );
}
```

## Core Patterns

### Controlled value

```tsx
const [country, setCountry] = useState<string | undefined>(undefined);

<Select value={country} onValueChange={setCountry} name="country">
  <SelectTrigger>
    <SelectValue placeholder="Choose…" />
  </SelectTrigger>
  <SelectPortal>
    <SelectContent>
      <SelectViewport>
        <SelectItem value="au">
          <SelectItemText>Australia</SelectItemText>
        </SelectItem>
        <SelectItem value="nz">
          <SelectItemText>New Zealand</SelectItemText>
        </SelectItem>
      </SelectViewport>
    </SelectContent>
  </SelectPortal>
</Select>
```

To clear selection, set `value` to `undefined` (not `""`).

### Popper positioning

`SelectContent` defaults to `position="item-aligned"` which centers the open list over the selected item (native select behavior). Set `position="popper"` for a floating dropdown anchored below the trigger.

```tsx
<SelectContent position="popper" sideOffset={4}>
  <SelectViewport>
    <SelectItem value="a"><SelectItemText>Option A</SelectItemText></SelectItem>
  </SelectViewport>
</SelectContent>
```

`position="popper"` enables the full Popper props: `side`, `sideOffset`, `align`, `alignOffset`, `collisionBoundary`, `collisionPadding`, `avoidCollisions`.

### Groups with labels

```tsx
<SelectViewport>
  <SelectGroup>
    <SelectLabel>Fruits</SelectLabel>
    <SelectItem value="apple"><SelectItemText>Apple</SelectItemText></SelectItem>
    <SelectItem value="banana"><SelectItemText>Banana</SelectItemText></SelectItem>
  </SelectGroup>
  <SelectSeparator />
  <SelectGroup>
    <SelectLabel>Vegetables</SelectLabel>
    <SelectItem value="carrot"><SelectItemText>Carrot</SelectItemText></SelectItem>
  </SelectGroup>
</SelectViewport>
```

## Common Mistakes

### 1. Passing empty string as SelectItem value — throws

`SelectItem` validates its `value` prop and throws if it is an empty string.

**Wrong:**

```tsx
<SelectItem value="">None</SelectItem>
```

**Correct:** Use a non-empty sentinel value, or leave `Select` uncontrolled with no `defaultValue` for an empty initial state.

```tsx
<SelectItem value="none">None</SelectItem>
```

Source: `src/components/select/select.tsx` — value validation

### 2. Missing SelectItemText — trigger shows nothing, typeahead fails

`SelectItemText` serves two purposes: its text is portaled into `SelectValue` when the item is selected, and it provides the string for keyboard typeahead matching. If omitted, the trigger renders blank and typing letters does not jump to items.

**Wrong:**

```tsx
<SelectItem value="apple">Apple</SelectItem>
```

**Correct:**

```tsx
<SelectItem value="apple">
  <SelectItemText>Apple</SelectItemText>
</SelectItem>
```

If you need an icon inside an item, put only the label text inside `SelectItemText`:

```tsx
<SelectItem value="apple">
  <SelectItemText>Apple</SelectItemText>
  <FruitIcon />
</SelectItem>
```

Source: `src/components/select/select.tsx` — `SelectItemText` portals into `SelectValue`

### 3. Not knowing the default is item-aligned, not popper

The default `position="item-aligned"` opens the list so the currently selected item sits directly over the trigger (like a native `<select>`). This can appear broken on first open if you expect a dropdown anchored below.

**Wrong assumption:** Select will always open downward like a `<div>` dropdown.

**Correct:** Set `position="popper"` explicitly when you want anchored floating behavior.

Source: `src/components/select/select.tsx` — `position` prop default `"item-aligned"`

### 4. Missing SelectViewport — items may not scroll

`SelectViewport` is the scroll container and injects cross-browser scrollbar-hiding styles. Without it, overflow on `SelectContent` may clip items or produce double scrollbars.

**Wrong:**

```tsx
<SelectContent>
  <SelectItem value="a"><SelectItemText>A</SelectItemText></SelectItem>
</SelectContent>
```

**Correct:**

```tsx
<SelectContent>
  <SelectViewport>
    <SelectItem value="a"><SelectItemText>A</SelectItemText></SelectItem>
  </SelectViewport>
</SelectContent>
```

Source: `src/components/select/select.tsx` — `SelectViewport` scroll container role

### 5. Using Select for async or filterable option lists

Select requires all `SelectItem` elements to be in the React tree at the time the dropdown opens. It has no built-in search input.

**Wrong:** Fetching options inside `SelectContent` on open, or expecting a search/filter input inside the select.

**Correct:** Compose `Popover` + `Command` for a combobox with async loading or text filtering. See the Command skill for the pattern.

Source: maintainer interview

## Cross-references

- [`references/select-components.md`](references/select-components.md) — full sub-component prop listing
- **Command** (`@loke/ui/command`) — async / searchable option lists via Popover + Command
- **Choosing the Right Component** — Select vs Popover+Command decision guide
