# Select (/docs/select)

A composable Base UI select with rich items, sizing, and positioning options.

## Install

```bash
npx shadcn@latest add https://4af.selfsimilar.dev/r/select.json
```

## Usage

```tsx
import {
  Select,
  SelectContent,
  SelectItem,
  SelectSeparator,
  SelectSub,
  SelectSubContent,
  SelectSubTrigger,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

<Select size="xs" defaultValue="apple">
  <SelectTrigger variant="ghost" label="Fruit">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectSeparator />
    <SelectSub>
      <SelectSubTrigger>More fruit</SelectSubTrigger>
      <SelectSubContent>
        <SelectItem value="banana">Banana</SelectItem>
      </SelectSubContent>
    </SelectSub>
  </SelectContent>
</Select>
```

## API

**Main exports:** `Select`, `SelectTrigger`, `SelectSortTrigger`, `SelectValue`, `SelectContent`,
`SelectItem`, `SelectSeparator`, `SelectSub`, `SelectSubTrigger`, `SelectSubContent`

An accessible Base UI single or multi-select with icons declarable on `items` so a preselected
value resolves before mount, optional in-panel search, a clear footer, content sizing, and the
compact sort/filter trigger.

### Trigger variants

`SelectTrigger` and `SelectSortTrigger` accept `variant="default" | "ghost" | "plain"`:

| `variant` | Trigger |
| --- | --- |
| `default` | The boxed field or chip. |
| `ghost` | Button's muted ghost colours with the default spacing, so leading and trailing icons stay aligned. |
| `plain` | No background, border, or horizontal padding; the dropdown icon follows the label or value directly. A sort trigger's `icon` stays leading. |

The focus ring remains for keyboard users. `outline={false}` is still accepted as an alias for
`variant="plain"`.

With neither `label` nor `showValue`, `SelectSortTrigger` shows only its glyph in a square box
matching Button's icon sizes: 24, 30, or 34px for `xs`, `sm`, or `md`. A toolbar can switch this
on the selection, so the chip names a filter only once one is set:

```tsx
<SelectSortTrigger icon={<ListFilter />} showValue={value !== null} aria-label="Sort" />
```

`SelectTrigger` also accepts an optional `label`, rendered as a dimmed prefix before the value.
For example, `label="Fruit"` displays `Fruit: Banana`.

When `SelectContent` has no explicit `width`, it follows the trigger: default and ghost triggers
keep a trigger-width panel, while plain triggers use content width — the longest option label plus
the reserved checkmark slot. Set `width="trigger"` or `width="content"` to override that default.

`SelectContent` opens with a row already active: the current selection (the most recent enabled
one in a multi-select), or the first enabled row when nothing is selected. Typing moves the
active row to the top enabled match. Disabled rows stay visible but are never the initial active
row or the search field's Enter target; when every match is disabled, no row is active. Set
`autoFocusFirstItem={false}` to opt out for menu-like controls such as a sort trigger.

`onValueChange(value, details)` can reject a selection with `details.cancel()`. The trigger,
checked rows, and the selection offered on reopening all retain the accepted value.

```tsx
<Select defaultValue="active" items={{ active: "Active", paused: "Paused" }}>
  <SelectTrigger variant="plain" label="Status">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="active">Active</SelectItem>
    <SelectItem value="paused">Paused</SelectItem>
  </SelectContent>
</Select>
```

## Sizing

The Select family uses the shared control scale: `size="xs"` is 24px for dense controls,
`size="sm"` is 30px with compact text and glyphs, and `size="md"` is 34px for form rows. The
selected trigger and every option row use the same size. The trigger uses a 6px button radius
at `xs`/`sm` and 8px at `md`.

## Nested select panels

Use `SelectSub` to put a second select panel behind a row in `SelectContent`. The trigger keeps
the chevron after its label. `ArrowRight` or `Enter` opens the panel; `ArrowLeft` or `Escape`
returns focus to the parent row. Its children are ordinary `SelectItem`s: they use the
same single- or multi-select value, check state, keyboard navigation, and close behavior as rows
in the main panel. When the parent `SelectContent` is searchable, its field also searches these
nested rows: a matching parent trigger remains in the main panel, and opening it reveals the
matching child rows. Nested panels can be nested again.

In a multi-select, each submenu trigger automatically shows a dimmed count of its selected child
items immediately before the chevron.

Nested panels open immediately when their parent row is hovered. Pass a positive `delay` to
`SelectSubTrigger` only when a deliberate hover pause is wanted. The initially focused nested row
stays unhighlighted until it is hovered or reached through keyboard navigation.

A hover-opened panel has a safe triangle. Leaving its row with the mouse keeps the panel open
while the pointer travels inside the triangle between the leave point and the panel's near edge,
so a diagonal path across the rows in between doesn't drop it. It closes as soon as the pointer
strays out of the triangle, or once it rests inside for 300ms. `closeDelay` still applies when
the pointer leaves the panel itself, and for non-mouse pointers.

```tsx
import {
  Select,
  SelectContent,
  SelectItem,
  SelectSub,
  SelectSubContent,
  SelectSubTrigger,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

<Select defaultValue="apple" items={{ apple: "Apple", banana: "Banana", mango: "Mango" }}>
  <SelectTrigger aria-label="Fruit">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectSub>
      <SelectSubTrigger>More fruit</SelectSubTrigger>
      <SelectSubContent>
        <SelectItem value="banana">Banana</SelectItem>
        <SelectItem value="mango">Mango</SelectItem>
      </SelectSubContent>
    </SelectSub>
  </SelectContent>
</Select>
```

## Separators

Use `SelectSeparator` directly between option groups. It is backed by Base UI's select
separator, carries the accessible `separator` role, and is not treated as a selectable row by
keyboard navigation or selection.

```tsx
import { SelectContent, SelectItem, SelectSeparator } from "@/components/ui/select";

<SelectContent>
  <SelectItem value="active">Active</SelectItem>
  <SelectSeparator />
  <SelectItem value="archived">Archived</SelectItem>
</SelectContent>
```

## Sort and filter trigger

`SelectSortTrigger` ships in `select`; there is no separate `select-sort` item. It:

- has a dashed border while no value is selected;
- becomes filled and solid once a value is selected, and while its panel is open;
- shows the selected label or labels from Select context;
- rotates its chevron like the normal Select trigger.

It takes the same `variant="ghost"` and `variant="plain"` options as `SelectTrigger`.

When `multiple` is enabled, the same trigger becomes a filter control and supports the nested
`SelectSub` panels described above. Nested rows participate in the multi-selection and keep the
panel open while choices are toggled.

Add `clearable` to `SelectContent` for a clear footer: a compact `xs` ghost reset button with a
`SelectSeparator` above it. The button is disabled when nothing is selected, and clearing keeps
the panel open. Hovering it moves the active-row highlight from the list to the button.

```tsx
import {
  Select,
  SelectContent,
  SelectItem,
  SelectSortTrigger,
} from "@/components/ui/select";

<Select<string>>
  <SelectSortTrigger label="Status" variant="plain" />
  <SelectContent clearable autoFocusFirstItem={false}>
    <SelectItem value="active">Active</SelectItem>
    <SelectItem value="paused">Paused</SelectItem>
  </SelectContent>
</Select>
```

`footerActions` adds buttons for actions on the selection, such as Archive or "Select all". They
share the footer row evenly and take the same hover highlight as `clearable`. Each takes a
`label`, an optional leading `icon`, `checked` to show a tick, and `keepOpen` to leave the panel
open. `clearable` adds its button after them.

`indicator={false}` hides the checkbox or tick; it does not prevent selection.

```tsx
<Select<string> multiple value={tags} onValueChange={setTags}>
  <SelectSortTrigger label="Tags" variant="plain" />
  <SelectContent
    searchable
    searchPlaceholder="Search tags"
    footerActions={[
      { key: "archive", label: "Archive", icon: <Archive />, checked: archived, onSelect: archive },
      { key: "clear", label: "Clear tags", icon: <X />, disabled: tags.length === 0, onSelect: clearTags },
    ]}
  >
    {options.map((option) => (
      <SelectItem key={option} value={option}>{option}</SelectItem>
    ))}
  </SelectContent>
</Select>
```

## Preselected icons

Declare option icons in the root `items` collection when a closed, preselected trigger must show
its icon on first paint:

```tsx
<Select
  items={[{ value: "sans", label: "Sans-serif", icon: <Type /> }]}
  defaultValue="sans"
>
```

`SelectItem`'s own `icon` only registers once the panel has mounted, so a closed trigger can't
show it on first load. Use `iconForValue` when the icon is computed rather than declared.
Resolution order is the mounted item, then `iconForValue`, then `items`.