Select
A form field that opens a list and takes exactly one value. It reads like Input at the call site: a label, an error message, and its own busyness.
className styles the wrapper
Select renders a wrapper around the trigger so it can hold the label and the error message, the same as Input. className styles that wrapper, not the trigger. Only Select, SelectItem, SelectGroup, SelectLabel, and SelectSeparator are exported — the Radix trigger, value, portal, content, and viewport stay internal.
Sizes
Two trigger sizes match Input and Button. Items inside the panel stay 32px at both sizes — a list is dense by nature and does not follow the trigger's height.
Groups, separators, and the checked item
SelectGroup takes a SelectLabel as its heading and separates from the next group with a SelectSeparator. The checked item shows a text-primary check in a reserved trailing icon slot, so every item label starts at the same left edge. An item's icon sits ahead of the label, hidden from screen readers so only the label is read, and stays in the list: the trigger shows the label alone. A disabled item stays in the list, dimmed, so its position never shifts. Open the trigger below to see all of it.
Error
Pass error and the trigger takes the destructive border and ring, the label turns destructive, and the message renders below the trigger — it may be covered while the panel is open, and reappears once it closes.
Choose a supported currency
Choose a supported currency
Choose a supported currency
Choose a supported currency
Loading
loading swaps the chevron for a spinner and keeps the trigger focusable with its value visible; opening is a no-op while the request runs. The field shows its own busyness, and the app shows the result. Choosing "Urgent" below fails a fake approval check and the field reports it through its own error; every other choice succeeds and the result shows inline through Alert, after the field settles.
Long lists scroll and truncate
The panel matches the trigger's width and scrolls inside a max height with no scroll buttons, so a long list like airports stays inside the viewport. Long item text truncates with an ellipsis, and the trigger truncates the chosen label the same way.
Motion
Hover border and focus ring are CSS transitions at --motion-fast, matching Input. The chevron rotates 180 degrees on open, a CSS transition at --motion-base — the one select-specific cue. The panel's enter and exit are the floating item's anchored keyframe pair: scale from 0.96 plus fade, 250ms on the bounce curve in and 150ms on the settle curve out, growing from the trigger. Menu items snap to their highlighted state with no transition, and the check mark shows no animation of its own — the panel closes the moment it appears.
Accessibility
Select generates the trigger id and wires the label itself, so clicking the label focuses the trigger without opening the panel. Enter, Space, ArrowUp, and ArrowDown open a closed trigger and never change the value on their own; once open, arrow keys, Home, End, and typeahead move the highlight, and Enter or Space chooses and closes. Escape and an outside click back out with no change, and focus always returns to the trigger. An error sets aria-invalid and links the message through aria-describedby. A loading trigger sets aria-busy and stays in the tab order; disabled removes it. A plain form posts the chosen value through Radix's hidden native <select>, and required marks the label, sets aria-required, and participates in native validation.