Select

Two shapes of the same idea. SelectInput is a field whose own text is the search. InlineSelect is a small one that lives inside another field.

Type to filter. The field is the search.
<SelectInput
  label="Country"
  options={[
    { value: 'fi', label: 'Finland' },
    { value: 'se', label: 'Sweden' },
  ]}
  hint="Type to filter. The field is the search."
/>

The country picker in the phone field puts a search box inside its panel, because the thing you clicked was a flag and a dial code, not somewhere to type. Here the thing you clicked is already a text field, so it filters. A second text box below the first would only be asking which one you meant.

value is the option's value, never the label on screen. Those are different things, and you should not have to parse a label back into an id. What you type while the panel is open is the panel's business and is discarded when it closes.

address-form.tsx
const [country, setCountry] = useState('fi')

<SelectInput
  label="Country"
  value={country}
  onChange={setCountry}
  options={COUNTRIES}
/>

Inline selector

A second decision attached to the value beside it: an access level on a name, a unit on a number. It goes in another field's trailing slot, which is why it has no label of its own and why aria-label is not optional.

<Input
  label="Share with"
  trailing={<InlineSelect aria-label="Access level" options={ACCESS} />}
/>

Keyboard

Both answer to the same keys as the country picker, because they are the same popover: arrows with wrap, Home and End, Enter to choose, Escape to close. The highlighted option is tracked with aria-activedescendant rather than by moving focus, so the field keeps it and typing never breaks.

SelectInput props

PropTypeDefaultNotes
optionsSelectOption[]{ value, label, keywords? }. `keywords` is matched but never shown.
leadingReactNodeA mark at the start of the field.
emptyLabelReactNode"No match"Shown when the query matches nothing.
requiredbooleanfalseRenders the asterisk and sets aria-required.
optionalbooleanfalseRenders the muted "(Optional)" note.
infostringInfo marker beside the label, with this as its tooltip.

Shared

PropTypeDefaultNotes
valuestringControlled value.
defaultValuestringInitial value when uncontrolled.
onChange(value: string) => voidReceives the value, never the event.
labelReactNodeRendered as a real <label> wired by htmlFor.
hintReactNodeHelper text under the field.
errorReactNodeError text. Implies invalid unless invalid says otherwise.
invalidbooleanForces the error styling on or off.
errorKeystring | numberChange it to replay the error animation.
disabledbooleanfalseGreys the field out.
readOnlybooleanfalseReads as filled, stays focusable.
size'sm' | 'md' | 'lg''md'Size scale, where the component defines one.
classNamesRecord<Part, string>Per-part class names.

InlineSelect props

Not a field, so it does not take the shared contract. It has no label, hint, error or size.

PropTypeDefaultNotes
optionsSelectOption[]The choices.
valuestringSelected value, controlled.
defaultValuestringfirst optionSelected value, uncontrolled.
onChange(value: string) => voidFires on selection.
aria-labelstringRequired: it has no visible label of its own.