Form
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"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.
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.
Groups and long list
Labelled groups with a separator; a list taller than the room scrolls, with scroll buttons at its ends.
Disabled
A disabled select keeps its value and does not open; a disabled item stays in the list and cannot be chosen.
Invalid
aria-invalid on the trigger draws the error border; aria-describedby reads the message with the name.
Accessibility
- Semantics
- The trigger is a
buttonwithrole="combobox",aria-expandedandaria-haspopup="listbox". The open list is arole="listbox"ofrole="option"items, the chosen one witharia-selected="true". Inside a form, a hidden nativeselectcarries the value, so it is submitted and autofilled. - Labels
- Name every select: a
LabelwithhtmlForset to the trigger'sid, oraria-labelwhen no visible text fits. Put an error message inaria-describedbyon 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 |
|---|---|---|---|
valuerequired | 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.
The tokens its classes read. Change their values in your theme, and it follows.
| 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 |