Select
One value chosen from a list. The trigger is a TextField's shell wearing a chevron — on purpose.
import { Select } from 'neba';
<Select
label="Region"
placeholder="Pick a region"
items={[
{ value: 'icn', label: 'Seoul' },
{ value: 'nrt', label: 'Tokyo' }
]}
/>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. The same shell as a TextField, so the two are indistinguishable in a form |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale. The same heights as Button and TextField |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. The surface is white, so it reaches the edge and the focus ring |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth of the trigger. The popup carries its own |
| items * | readonly SelectOption[] | — | The options, as an array of { value, label?, disabled? } |
| value | string | number | null | — | The chosen value. Use with onValueChange for a controlled select |
| defaultValue | string | number | null | — | The initial value, for an uncontrolled select |
| onValueChange | (value: string | number | null) => void | — | Called when the chosen value changes |
| placeholder | ReactNode | — | Shown in the trigger while nothing is chosen |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| startIcon | ReactNode | — | Content before the value |
| fullWidth | boolean | false | Stretches to the width of the container |
| name | string | — | Identifies the field when a form is submitted |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
Options are data
There is no <Select.Option> to compose. What a caller has is almost always an array already, and the list has to be available to the trigger before the popup has ever been opened — that is how Seoul shows up for value="icn" on first paint.
interface SelectOption {
value: string | number;
label?: React.ReactNode; // defaults to the value
disabled?: boolean;
}Values are strings and numbers, not arbitrary objects. A select is a form control and its value is what gets submitted; keep the identifier here and look the object up on the other side.
Examples
Variants
The same three weights a TextField has, drawn on the same shell. A form where the select is a different height, radius or colour from the fields around it is a form that looks assembled rather than designed.
Sizes
States
The popup
The popup is the one surface in the library that is supposed to float, so unlike everything else it carries a shadow without being asked — at level 3, which is as far as the scale goes without hovering.
It renders in a portal, at the end of <body>, which means it leaves any subtree your app scoped a CSS reset to. The positioner carries a neba-portal class for exactly that case: it is a hook to hang the reset off, not a style of its own. An app with Tailwind's Preflight applied globally needs nothing.
Accessibility
- Base UI owns the popup's positioning and flipping, the focus trap, typeahead and the hidden input that makes the select submit with a form.
labelbecomes the accessible name; the trigger is acombobox.- A disabled option stays in the list and reports
aria-disabled— the option exists, it just cannot be picked.