Skip to content

Select

One value chosen from a list. The trigger is a TextField's shell wearing a chevron — on purpose.

tsx
import { Select } from 'neba';

<Select
  label="Region"
  placeholder="Pick a region"
  items={[
    { value: 'icn', label: 'Seoul' },
    { value: 'nrt', label: 'Tokyo' }
  ]}
/>;

Props

PropTypeDefaultDescription
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
elevationshared0 | 1 | 2 | 30Drop shadow depth of the trigger. The popup carries its own
items * readonly SelectOption[]The options, as an array of { value, label?, disabled? }
valuestring | number | nullThe chosen value. Use with onValueChange for a controlled select
defaultValuestring | number | nullThe initial value, for an uncontrolled select
onValueChange(value: string | number | null) => voidCalled when the chosen value changes
placeholderReactNodeShown in the trigger while nothing is chosen
labelReactNodeThe label, wired to the control by Base UI's Field
descriptionReactNodeHelper text
errorReactNodeError message. Its presence turns the control invalid and re-points the colour family at danger
invalidboolean!!errorForces the invalid state without a message, for when a form library owns validity
startIconReactNodeContent before the value
fullWidthbooleanfalseStretches to the width of the container
namestringIdentifies the field when a form is submitted
readOnlybooleanfalseShown but not changeable. Keeps its colour and edge, drains the saturation
disabledbooleanfalseUnavailable. 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.

ts
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.
  • label becomes the accessible name; the trigger is a combobox.
  • A disabled option stays in the list and reports aria-disabled — the option exists, it just cannot be picked.

Released under the MIT License