# Select

> A dropdown for choosing from a list of options

Status: stable. Added in Components 2.0.0. Updated in Components 2.16.0, 20 Oct 2026.

A dropdown for choosing from a list of options.

## Import

```tsx
import { Select } from '@oztix/roadie-components/select'
```

## Examples

### Default

Pass string children to `Select.Item` for automatic `ItemText` + `ItemIndicator`
wrapping. Use `Select.Content` as a convenience wrapper for Portal + Positioner + Popup.

```tsx
<Select defaultValue='powderfinger'>
  <Select.Trigger>
    <Select.Value placeholder='Pick a band' />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='powderfinger'>Powderfinger</Select.Item>
    <Select.Item value='custard'>Custard</Select.Item>
    <Select.Item value='regurgitator'>Regurgitator</Select.Item>
    <Select.Item value='violent-soho'>Violent Soho</Select.Item>
    <Select.Item value='dz-deathrays'>DZ Deathrays</Select.Item>
  </Select.Content>
</Select>
```

### Emphasis

`normal` is the default: a plain fill with a visible border, like a normal `Button`. `subtle` sits on a tinted fill. `subtler` has no fill or border at rest, for quiet controls inside something else, such as rows per page under a table.

```tsx
<Select defaultValue='go-betweens'>
  <Select.Trigger emphasis='normal'>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='go-betweens'>The Go-Betweens</Select.Item>
    <Select.Item value='saints'>The Saints</Select.Item>
  </Select.Content>
</Select>
<Select defaultValue='bee-gees'>
  <Select.Trigger emphasis='subtle'>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='bee-gees'>Bee Gees</Select.Item>
    <Select.Item value='savage-garden'>Savage Garden</Select.Item>
  </Select.Content>
</Select>
<div className='flex items-center justify-between gap-3 text-sm text-subtle'>
  <p>1–50 of 240</p>
  <Select defaultValue='50'>
    <Select.Trigger
      aria-label='Rows per page'
      emphasis='subtler'
      size='sm'
    >
      <Select.Value />
      <Select.Icon />
    </Select.Trigger>
    <Select.Content>
      <Select.Item value='50'>50 per page</Select.Item>
      <Select.Item value='100'>100 per page</Select.Item>
    </Select.Content>
  </Select>
</div>
```

### Sizes

```tsx
<Select defaultValue='ball-park-music'>
  <Select.Trigger size='sm'>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='ball-park-music'>Ball Park Music</Select.Item>
  </Select.Content>
</Select>
<Select defaultValue='cub-sport'>
  <Select.Trigger size='md'>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='cub-sport'>Cub Sport</Select.Item>
  </Select.Content>
</Select>
<Select defaultValue='jungle-giants'>
  <Select.Trigger size='lg'>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='jungle-giants'>The Jungle Giants</Select.Item>
  </Select.Content>
</Select>
```

### States

`normal` and `subtle` triggers use `is-interactive-field` for state-based
colour transitions: neutral at rest, accent on focus/open, danger when invalid.
A `subtler` trigger uses `is-interactive` instead, so it behaves like a subtler
`Button`: no fill at rest, a light fill on hover, press and while open, and a
danger edge when invalid.

```tsx
{/* Default */}
<Select defaultValue='goon-sax'>
  <Select.Trigger>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='goon-sax'>The Goon Sax</Select.Item>
  </Select.Content>
</Select>
{/* Disabled */}
<Select defaultValue='powderfinger' disabled>
  <Select.Trigger>
    <Select.Value />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value='powderfinger'>Powderfinger</Select.Item>
  </Select.Content>
</Select>
```

### Composition

#### With Field

Wrap Select in [Field](https://ticketsolutionsptyltd.github.io/roadie/components/field.md) for consistent layout, labels, and
error handling. Field provides spacing, label wiring, and context flow
automatically.

```tsx
<Field required>
  <Field.Label showIndicator>Favourite band</Field.Label>
  <Select defaultValue='powderfinger'>
    <Select.Trigger>
      <Select.Value placeholder='Pick a band' />
      <Select.Icon />
    </Select.Trigger>
    <Select.Content>
      <Select.Item value='powderfinger'>Powderfinger</Select.Item>
      <Select.Item value='custard'>Custard</Select.Item>
      <Select.Item value='regurgitator'>Regurgitator</Select.Item>
    </Select.Content>
  </Select>
</Field>
```

#### With Field and error

```tsx
<Field invalid required>
  <Field.Label showIndicator>Favourite band</Field.Label>
  <Select>
    <Select.Trigger>
      <Select.Value placeholder='Pick a band' />
      <Select.Icon />
    </Select.Trigger>
    <Select.Content>
      <Select.Item value='powderfinger'>Powderfinger</Select.Item>
      <Select.Item value='custard'>Custard</Select.Item>
      <Select.Item value='regurgitator'>Regurgitator</Select.Item>
    </Select.Content>
  </Select>
  <Field.ErrorText>Choose a band.</Field.ErrorText>
</Field>
```

#### With standalone label

Select also has its own `Select.Label` for standalone usage without a Field
wrapper. Use `showIndicator` to show a required or optional indicator.

```tsx
<Select defaultValue='powderfinger' required>
  <div className='grid gap-1.5'>
    <Select.Label showIndicator>Favourite band</Select.Label>
    <Select.Trigger>
      <Select.Value placeholder='Pick a band' />
      <Select.Icon />
    </Select.Trigger>
  </div>
  <Select.Content>
    <Select.Item value='powderfinger'>Powderfinger</Select.Item>
    <Select.Item value='custard'>Custard</Select.Item>
    <Select.Item value='regurgitator'>Regurgitator</Select.Item>
  </Select.Content>
</Select>
```

#### With groups

```tsx
<Select defaultValue='powderfinger'>
  <Select.Trigger>
    <Select.Value placeholder='Pick a band' />
    <Select.Icon />
  </Select.Trigger>
  <Select.Content>
    <Select.Group>
      <Select.GroupLabel>Brisbane</Select.GroupLabel>
      <Select.Item value='powderfinger'>Powderfinger</Select.Item>
      <Select.Item value='custard'>Custard</Select.Item>
      <Select.Item value='go-betweens'>The Go-Betweens</Select.Item>
      <Select.Item value='saints'>The Saints</Select.Item>
    </Select.Group>
    <Select.Group>
      <Select.GroupLabel>Gold Coast</Select.GroupLabel>
      <Select.Item value='violent-soho'>Violent Soho</Select.Item>
      <Select.Item value='dz-deathrays'>DZ Deathrays</Select.Item>
      <Select.Item value='goon-sax'>The Goon Sax</Select.Item>
    </Select.Group>
  </Select.Content>
</Select>
```

#### With multiple selection

Set `multiple` to let people pick more than one option. The popup stays open
between picks. The trigger lists the labels that fit and counts the rest, such
as "Bee Gees, Custard +2", while screen readers hear every label. `value`,
`defaultValue` and `onValueChange` then take an array.

```tsx
<Field>
  <Field.Label>Bands to see</Field.Label>
  <Select multiple defaultValue={['bee-gees', 'custard']}>
    <Select.Trigger>
      <Select.Value placeholder='Pick bands' />
      <Select.Icon />
    </Select.Trigger>
    <Select.Content>
      <Select.Item value='bee-gees'>Bee Gees</Select.Item>
      <Select.Item value='custard'>Custard</Select.Item>
      <Select.Item value='powderfinger'>Powderfinger</Select.Item>
      <Select.Item value='regurgitator'>Regurgitator</Select.Item>
    </Select.Content>
  </Select>
</Field>
```

## Guidelines

### When to use Select

Use Select when the list of options is **known, short, and fixed**. The user
picks from a closed set. They don't need to search or type a custom value.

- Country/state dropdowns (small set)
- Status or category pickers
- Sort-by controls
- Any list under \~15 items where scanning is faster than typing

### Choosing an emphasis

#### Save subtler for in-context controls

**Do**

```tsx
<Select.Trigger emphasis='subtler' size='sm' />
```

Use `subtler` for a control that adjusts what is already on screen, such as rows per page, sort order or a chart's comparison, beside other subtler buttons. A subtler trigger takes only the width of its value and icon, where normal and subtle fill their container. If the value changes length, hold the width with the widest label so the controls beside it stay put.

**Don’t**

Don't use `subtler` for a select in a form. With no border at rest, it doesn't read as a field to fill in next to an `Input`.

### When to use Combobox instead

If users are likely to **search** the list, the list is **long** (15+ items), or
options load **asynchronously**, use [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md).

| Question                                   | Select                  | Combobox          |
| ------------------------------------------ | ----------------------- | ----------------- |
| Can the user type to filter?               | No (typeahead only)     | Yes               |
| Is the list longer than \~15 items?        | Avoid                   | Preferred         |
| Are options loaded from an API?            | No                      | Yes               |
| Can the user enter a custom value?         | No                      | Possible          |
| Does the user know roughly what they want? | Maybe not. They browse. | Yes. They search. |

### Keyboard behaviour

Select delegates all keyboard handling to Base UI:

- **Arrow keys** navigate the list
- **Typeahead** jumps to items by first letter(s)
- **Enter / Space** confirms the highlighted item
- **Escape** closes the popup

### Accessibility

- Always pair with `Select.Label` or an `aria-label` on the trigger.
- `Select.ItemIndicator` provides a visible check for the selected item. Include it for clarity.
- The popup locks scroll by default (`modal={true}`). Set `modal={false}` if the select appears in a non-blocking context like a toolbar.

## API reference

### Select

```tsx
import { Select } from '@oztix/roadie-components/select'
```

- `inputRef`: `Ref<HTMLInputElement>`. A ref to access the hidden input element.
- `name`: `string`. Identifies the field when a form is submitted.
- `form`: `string`. Identifies the form that owns the hidden input. Useful when the select is rendered outside the form.
- `autoComplete`: `string`. Provides a hint to the browser for autofill. @see [https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete)
- `id`: `string`. The id of the Select.
- `required`: `boolean`. Defaults to `false`. Whether the user must choose a value before submitting a form.
- `readOnly`: `boolean`. Defaults to `false`. Whether the user should be unable to choose a different option from the select popup.
- `disabled`: `boolean`. Defaults to `false`. Whether the component should ignore user interaction.
- `multiple`: `boolean`. Defaults to `false`. Whether multiple items can be selected.
- `highlightItemOnHover`: `boolean`. Defaults to `true`. Whether moving the pointer over items should highlight them. Disabling this prop allows CSS `:hover` to be differentiated from the `:focus` (`data-highlighted`) state.
- `defaultOpen`: `boolean`. Defaults to `false`. Whether the select popup is initially open. To render a controlled select popup, use the `open` prop instead.
- `onOpenChange`: `((open: boolean, eventDetails: SelectRootOpenChangeEventDetails) => void)`. Event handler called when the select popup is opened or closed.
- `onOpenChangeComplete`: `((open: boolean) => void)`. Event handler called after any animations complete when the select popup is opened or closed.
- `open`: `boolean`. Whether the select popup is currently open.
- `modal`: `boolean`. Defaults to `true`. Determines if the select enters a modal state when open. - `true`: user interaction is limited to the select: document page scroll is locked and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. On touch devices, a `true` modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior.
- `actionsRef`: `RefObject<SelectRootActions | null>`. A ref to imperative actions. - `unmount`: Ends the closing phase of the select after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the select completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the select imperatively when called.
- `items`: `Record<string, ReactNode> | readonly { label: ReactNode; value: any; }[] | readonly Group<any>[]`. Data structure of the items rendered in the select popup. When specified, `<Select.Value>` renders the label of the selected item instead of the raw value. @example `tsx const items = { sans: 'Sans-serif', serif: 'Serif', mono: 'Monospace', cursive: 'Cursive', }; <Select.Root items={items} /> `
- `itemToStringLabel`: `((itemValue: Value) => string)`. When the item values are objects (`<Select.Item value={object}>`), this function converts the object value to a string representation for display in the trigger. If the shape of the object is `{ value, label }`, the label will be used automatically without needing to specify this prop.
- `itemToStringValue`: `((itemValue: Value) => string)`. When the item values are objects (`<Select.Item value={object}>`), this function converts the object value to a string representation for form submission. If the shape of the object is `{ value, label }`, the value will be used automatically without needing to specify this prop.
- `isItemEqualToValue`: `((itemValue: Value, value: Value) => boolean)`. Custom comparison logic used to determine if a select item value matches the current selected value. Useful when item values are objects without matching referentially. Defaults to `Object.is` comparison.
- `defaultValue`: `SelectInputValue<Value, Multiple> | null`. The uncontrolled value of the select when it's initially rendered. To render a controlled select, use the `value` prop instead.
- `value`: `SelectInputValue<Value, Multiple> | null`. The value of the select. Use when controlled.
- `onValueChange`: `((value: SelectOutputValue<Value, Multiple> | (Multiple extends true ? never : null), eventDetails: SelectRootChangeEventDetails) => void)`. Event handler called when the value of the select changes.
- `invalid`: `boolean`.

### Select.Content

- `finalFocus`: `boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)`. Determines the element to focus when the select popup is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.
- `className`: `string | ((state: SelectPopupState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.ErrorText

No props beyond the standard HTML attributes.

### Select.Group

- `className`: `string | ((state: SelectGroupState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.GroupLabel

- `className`: `string | ((state: SelectGroupLabelState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.HelperText

No props beyond the standard HTML attributes.

### Select.Icon

- `className`: `string | ((state: SelectIconState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.Item

- `value`: `any`. Defaults to `null`. A unique value that identifies this select item.
- `disabled`: `boolean`. Defaults to `false`. Whether the component should ignore user interaction.
- `label`: `string`. Specifies the text label to use when the item is matched during keyboard text navigation. Defaults to the item text content if not provided.
- `nativeButton`: `boolean`. Defaults to `false`. Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `true` if the rendered element is a native button.
- `className`: `string | ((state: SelectItemState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.ItemIndicator

- `keepMounted`: `boolean`. Whether to keep the HTML element in the DOM when the item is not selected.
- `className`: `string | ((state: SelectItemIndicatorState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.ItemText

- `className`: `string | ((state: SelectItemTextState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.Label

- `className`: `string | ((state: FieldRootState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.
- `showIndicator`: `boolean`.

### Select.Popup

- `finalFocus`: `boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)`. Determines the element to focus when the select popup is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.
- `className`: `string | ((state: SelectPopupState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.Portal

- `container`: `HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null`. A parent element to render the portal element into.
- `className`: `string | ((state: SelectPortalState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.Positioner

- `alignItemWithTrigger`: `boolean`. Defaults to `true`. Whether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space.
- `anchor`: `Element | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null`. An element to position the popup against. By default, the popup will be positioned against the trigger.
- `positionMethod`: `"absolute" | "fixed"`. Defaults to `'absolute'`. Determines which CSS `position` property to use.
- `side`: `"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"`. Defaults to `'bottom'`. Which side of the anchor element to align the popup against. May automatically change to avoid collisions.
- `sideOffset`: `number | OffsetFunction`. Defaults to `0`. Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. @example `jsx <Positioner sideOffset={({ side, align, anchor, positioner }) => { return side === 'top' || side === 'bottom' ? anchor.height : anchor.width; }} /> `
- `align`: `"center" | "start" | "end"`. Defaults to `'center'`. How to align the popup relative to the specified side.
- `alignOffset`: `number | OffsetFunction`. Defaults to `0`. Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. @example `jsx <Positioner alignOffset={({ side, align, anchor, positioner }) => { return side === 'top' || side === 'bottom' ? anchor.width : anchor.height; }} /> `
- `collisionBoundary`: `Boundary`. Defaults to `'clipping-ancestors'`. An element or a rectangle that delimits the area that the popup is confined to.
- `collisionPadding`: `Padding`. Defaults to `5`. Additional space to maintain from the edge of the collision boundary.
- `sticky`: `boolean`. Defaults to `false`. Whether to maintain the popup in the viewport after the anchor element was scrolled out of view.
- `arrowPadding`: `number`. Defaults to `5`. Minimum distance to maintain between the arrow and the edges of the popup. Use it to prevent the arrow element from hanging out of the rounded corners of a popup.
- `disableAnchorTracking`: `boolean`. Defaults to `false`. Whether to disable the popup from tracking any layout shift of its positioning anchor.
- `collisionAvoidance`: `CollisionAvoidance`. Determines how to handle collisions when positioning the popup. `side` controls overflow on the preferred placement axis (`top`/`bottom` or `left`/`right`): - `'flip'`: keep the requested side when it fits; otherwise try the opposite side (`top` and `bottom`, or `left` and `right`). - `'shift'`: never change side; keep the requested side and move the popup within the clipping boundary so it stays visible. - `'none'`: do not correct side-axis overflow. `align` controls overflow on the alignment axis (`start`/`center`/`end`): - `'flip'`: keep side, but swap `start` and `end` when the requested alignment overflows. - `'shift'`: keep side and requested alignment, then nudge the popup along the alignment axis to fit. - `'none'`: do not correct alignment-axis overflow. `fallbackAxisSide` controls fallback behavior on the perpendicular axis when the preferred axis cannot fit: - `'start'`: allow perpendicular fallback and try the logical start side first (`top` before `bottom`, or `left` before `right` in LTR). - `'end'`: allow perpendicular fallback and try the logical end side first (`bottom` before `top`, or `right` before `left` in LTR). - `'none'`: do not fallback to the perpendicular axis. When `side` is `'shift'`, explicitly setting `align` only supports `'shift'` or `'none'`. If `align` is omitted, it defaults to `'flip'`. @example `jsx <Positioner collisionAvoidance={{ side: 'shift', align: 'shift', fallbackAxisSide: 'none', }} /> `
- `className`: `string | ((state: SelectPositionerState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.ScrollDownArrow

- `keepMounted`: `boolean`. Defaults to `false`. Whether to keep the HTML element in the DOM while the select popup is not scrollable.
- `className`: `string | ((state: SelectScrollDownArrowState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.ScrollUpArrow

- `keepMounted`: `boolean`. Defaults to `false`. Whether to keep the HTML element in the DOM while the select popup is not scrollable.
- `className`: `string | ((state: SelectScrollUpArrowState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

### Select.Trigger

- `disabled`: `boolean`. Whether the component should ignore user interaction.
- `nativeButton`: `boolean`. Defaults to `false`. Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `true` if the rendered element is a native button.
- `className`: `string | ((state: SelectTriggerState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.
- `emphasis`: `"normal" | "subtle" | "subtler" | null`. Defaults to `'normal'`. `subtler` drops the fill and border for quiet in-context controls, such as rows per page.
- `size`: `"sm" | "md" | "lg" | null`. Defaults to `'md'`.
- `intent`: `"neutral" | "brand" | "brand-secondary" | "brand-blue" | "brand-orange" | "brand-purple" | "accent" | "danger" | "success" | "warning" | "info" | null`. Deprecated: Form controls take their colour from state; `is-interactive-field` handles it. Will be removed in v3.0.0.

### Select.Value

- `children`: `ReactNode | ((value: any) => ReactNode)`. Accepts a function that returns a `ReactNode` to format the selected value. Treat the value as read-only: in `multiple` mode it may be a shared frozen array when nothing is selected. @example `tsx <Select.Value> {(value: string | null) => value ? labels[value] : 'No value'} </Select.Value> `
- `placeholder`: `ReactNode`. The placeholder value to display when no value is selected. This is overridden by `children` if specified, or by a null item's label in `items`.
- `className`: `string | ((state: SelectValueState) => string)`. CSS class applied to the element, or a function that returns a class based on the component's state.

Docs updated 19 Oct 2026.
