# Select

Lets people choose one option from a list that opens below the field.

- **Import:** `import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "@booleanpress/ui/select"`
- **Radix Select:** <https://www.radix-ui.com/primitives/docs/components/select>
- **APG Select-only combobox:** <https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/>
- **Page:** <https://ui.booleanpress.com/components/select> · @booleanpress/ui 0.1.0

## Usage

Give the field a visible name: a `Label` whose `htmlFor` is the trigger's `id`. The list opens below the field, at least as wide as it, and flips above when there is no room. This differs from stock, where the list covers the field (`position="item-aligned"`); pass `position="item-aligned"` to `SelectContent` to get that back.

```tsx
import { Label } from "@booleanpress/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@booleanpress/ui/select"

export function MailerSelect() {
  return (
    <>
      <Label htmlFor="mailer">Mailer</Label>
      <Select defaultValue="ses">
        <SelectTrigger id="mailer">
          <SelectValue placeholder="Choose a mailer" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="ses">Amazon SES</SelectItem>
          <SelectItem value="postmark">Postmark</SelectItem>
        </SelectContent>
      </Select>
    </>
  )
}
```

It is uncontrolled with `defaultValue`, or controlled with `value` and `onValueChange`. The value is always a string, and an item's `value` cannot be an empty string. `SelectTrigger` is as wide as its content; add `className="w-full"` to fill the column. Group items with `SelectGroup`, `SelectLabel` and `SelectSeparator`. For a list that people search, use a combobox built from `Command` and `Popover`; for a long list of native options on a phone, `NativeSelect` is lighter.

## Examples

### Basic

A named field with a preselected option and a placeholder for the empty state.

```tsx
import { Label } from "@booleanpress/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@booleanpress/ui/select"

export default function SelectBasic() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="mailer">Mailer</Label>
      <Select defaultValue="ses">
        <SelectTrigger id="mailer" className="w-full">
          <SelectValue placeholder="Choose a mailer" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="ses">Amazon SES</SelectItem>
          <SelectItem value="sendgrid">SendGrid</SelectItem>
          <SelectItem value="postmark">Postmark</SelectItem>
        </SelectContent>
      </Select>
    </div>
  )
}
```

### Groups and long list

Labelled groups with a separator; a list taller than the room scrolls, with scroll buttons at its ends.

```tsx
import { Label } from "@booleanpress/ui/label"
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from "@booleanpress/ui/select"

const API_MAILERS = ["Amazon SES", "Brevo", "Mailgun", "Postmark", "SendGrid", "SparkPost", "Resend", "Mailjet"]
const SMTP_MAILERS = ["Gmail SMTP", "Outlook SMTP", "Zoho SMTP", "Custom SMTP"]

export default function SelectGroups() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="mailer-groups">Mailer</Label>
      <Select>
        <SelectTrigger id="mailer-groups" className="w-full">
          <SelectValue placeholder="Choose a mailer" />
        </SelectTrigger>
        <SelectContent>
          <SelectGroup>
            <SelectLabel>API mailers</SelectLabel>
            {API_MAILERS.map((name) => (
              <SelectItem key={name} value={name}>
                {name}
              </SelectItem>
            ))}
          </SelectGroup>
          <SelectSeparator />
          <SelectGroup>
            <SelectLabel>SMTP</SelectLabel>
            {SMTP_MAILERS.map((name) => (
              <SelectItem key={name} value={name}>
                {name}
              </SelectItem>
            ))}
          </SelectGroup>
        </SelectContent>
      </Select>
    </div>
  )
}
```

### Disabled

A disabled select keeps its value and does not open; a disabled item stays in the list and cannot be chosen.

```tsx
import { Label } from "@booleanpress/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@booleanpress/ui/select"

export default function SelectDisabled() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Label htmlFor="whole-disabled">Fallback mailer</Label>
        <Select disabled defaultValue="none">
          <SelectTrigger id="whole-disabled" className="w-full">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="none">None</SelectItem>
            <SelectItem value="ses">Amazon SES</SelectItem>
          </SelectContent>
        </Select>
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="item-disabled">Region</Label>
        <Select defaultValue="eu-west-1">
          <SelectTrigger id="item-disabled" className="w-full">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="eu-west-1">Europe (Ireland)</SelectItem>
            <SelectItem value="us-east-1">US East (N. Virginia)</SelectItem>
            <SelectItem value="ap-south-1" disabled>
              Asia Pacific (Mumbai)
            </SelectItem>
          </SelectContent>
        </Select>
      </div>
    </div>
  )
}
```

### Invalid

`aria-invalid` on the trigger draws the error border; `aria-describedby` reads the message with the name.

```tsx
import { Label } from "@booleanpress/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@booleanpress/ui/select"

export default function SelectInvalid() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="mailer-invalid">Mailer</Label>
      <Select required>
        <SelectTrigger id="mailer-invalid" className="w-full" aria-invalid aria-describedby="mailer-error">
          <SelectValue placeholder="Choose a mailer" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="ses">Amazon SES</SelectItem>
          <SelectItem value="postmark">Postmark</SelectItem>
        </SelectContent>
      </Select>
      <p id="mailer-error" className="text-sm text-destructive">
        Choose a mailer to send the test email.
      </p>
    </div>
  )
}
```

## Accessibility

**Semantics.** The trigger is a `button` with `role="combobox"`, `aria-expanded` and `aria-haspopup="listbox"`. The open list is a `role="listbox"` of `role="option"` items, the chosen one with `aria-selected="true"`. Inside a form, a hidden native `select` carries the value, so it is submitted and autofilled.

**Labels.** Name every select: a `Label` with `htmlFor` set to the trigger's `id`, or `aria-label` when no visible text fits. Put an error message in `aria-describedby` on the trigger.

**Focus.** The trigger is in the tab order. Opening moves focus to the chosen option, or to the first; closing returns it to the trigger. While the list is open, the rest of the page is hidden from assistive technology and scroll is locked.

**Known limits.**

- The list is a portal, outside the trigger's container, so CSS that targets the container does not reach it.
- There is no search box. Typing selects by the first letters of the labels (type-ahead) and nothing else.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Enter | On the trigger, opens the list. In the list, chooses the focused option and closes it. |
| Space | On the trigger, opens the list. In the list, chooses the focused option. |
| ArrowDown | On the trigger, opens the list. In the list, moves focus to the next option. |
| ArrowUp | On the trigger, opens the list. In the list, moves focus to the previous option. |
| Home | In the list, moves focus to the first option. |
| End | In the list, moves focus to the last option. |
| A–Z | Type-ahead: moves focus to the next option whose label starts with the letters typed. |
| Escape | Closes the list without changing the value, and returns focus to the trigger. |

## API

### Select

Renders Radix Select.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `autoComplete` | `string` |  |  |
| `defaultOpen` | `boolean` |  | Whether the list starts open. |
| `defaultValue` | `string` |  | The value it starts with, when it controls itself. |
| `dir` | `"ltr" \| "rtl"` |  | Reading direction of the list. Defaults to the provider's. |
| `disabled` | `boolean` |  | Stops the select from opening and takes it out of the tab order. |
| `form` | `string` |  |  |
| `name` | `string` |  | The field name, for the value the form submits. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called when the list opens or closes. |
| `onValueChange` | `((value: string) => void)` |  | Called with the new value when an option is chosen. |
| `open` | `boolean` |  | Whether the list is open, when you control it. Pair it with `onOpenChange`. |
| `required` | `boolean` |  | The form cannot be submitted until a value is chosen. |
| `value` | `string` |  | The chosen value, when you control it. Pair it with `onValueChange`. |

### SelectContent

Renders Radix Select.Content and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"center" \| "start" \| "end"` |  | How the list lines up with the trigger on the cross axis in `popper` mode: `start`, `center` or `end`. |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `asChild` | `boolean` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"left" \| "right" \| "top" \| "bottom", number>>` |  |  |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `hideWhenDetached` | `boolean` |  |  |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Event handler called when the escape key is down. Can be prevented. |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
| `position` | `"item-aligned" \| "popper"` | `popper` | `popper` (the default here) opens below the field and flips above when there is no room; `item-aligned` is stock: the list covers the field and aligns the chosen option with it. |
| `side` | `"left" \| "right" \| "top" \| "bottom"` |  | Which side of the trigger the list prefers in `popper` mode. It flips when there is no room. |
| `sideOffset` | `number` | `4` | Distance in pixels between the trigger and the list in `popper` mode. |
| `sticky` | `"partial" \| "always"` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

### SelectGroup

Renders Radix Select.Group and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### SelectItem

Renders Radix Select.Item and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  | The value chosen by this item. It cannot be an empty string. |
| `asChild` | `boolean` |  |  |
| `disabled` | `boolean` |  | The item cannot be chosen or focused. |
| `textValue` | `string` |  | Text used for type-ahead and for the trigger's display, when the children are not plain text. |

### SelectLabel

Renders Radix Select.Label and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### SelectScrollDownButton

Renders Radix Select.ScrollDownButton and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### SelectScrollUpButton

Renders Radix Select.ScrollUpButton and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### SelectSeparator

Renders Radix Select.Separator and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### SelectTrigger

Renders Radix Select.Trigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `size` | `"default" \| "sm"` | `default` | `default` is 36 px high, `sm` is 32 px. |

### SelectValue

Renders Radix Select.Value and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `placeholder` | `ReactNode` |  | Shown while no value is chosen, in the muted colour. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="select"` (Select), `data-slot="select-content"` (SelectContent), `data-slot="select-group"` (SelectGroup), `data-slot="select-item"` (SelectItem), `data-slot="select-label"` (SelectLabel), `data-slot="select-scroll-down-button"` (SelectScrollDownButton), `data-slot="select-scroll-up-button"` (SelectScrollUpButton), `data-slot="select-separator"` (SelectSeparator), `data-slot="select-trigger"` (SelectTrigger), `data-slot="select-value"` (SelectValue), and `data-size`.

## Theming

The trigger takes `--control` for the border (at least 3:1 against the surface), `--ring` for focus and `--destructive` for the invalid state. The list uses `--popover`, `--popover-foreground` and `--accent` for the focused option. Its enter and exit motion is set once in `theme.css`.

| Token | Used for |
| --- | --- |
| `--accent` | background |
| `--accent-foreground` | text |
| `--border` | background |
| `--control` | border |
| `--destructive` | border, focus ring |
| `--input` | border, background |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
| `--ring` | border, focus ring |
