Select

Accessible select built from scratch. Three modes: single, searchable (filter input), and multi-select (toggle items with checkmarks). Full keyboard navigation, ARIA, and a portaled listbox anchored to the trigger.

Single

import {
  SelectContent,
  SelectItem,
  SelectRoot,
  SelectTrigger,
  SelectValue,
} from '@bwo-ui/react';

<SelectRoot defaultValue="apple">
  <SelectTrigger>
    <SelectValue placeholder="Pick a fruit…" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
    <SelectItem value="cherry">Cherry</SelectItem>
  </SelectContent>
</SelectRoot>

Searchable

<SelectRoot searchable searchPlaceholder="Filter frameworks…">
  <SelectTrigger>
    <SelectValue placeholder="Pick a stack…" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="next">Next.js</SelectItem>
    <SelectItem value="remix">Remix</SelectItem>
    <SelectItem value="vite">Vite</SelectItem>
    {/* …items hide themselves when they don't match */}
  </SelectContent>
</SelectRoot>

Items filter themselves by their string children. Override with the searchText prop on SelectItem when the children are JSX.

Multiple

const [picked, setPicked] = useState<string[]>(['design', 'frontend']);

<SelectRoot multiple value={picked} onValueChange={setPicked}>
  <SelectTrigger>
    <SelectValue
      placeholder="Pick roles…"
      formatMultiple={(_, vs) =>
        vs.length <= 2 ? vs.join(', ') : `${vs.length} selected`
      }
    />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="design">Design</SelectItem>
    <SelectItem value="frontend">Frontend</SelectItem>
    <SelectItem value="backend">Backend</SelectItem>
  </SelectContent>
</SelectRoot>

When multiple is set, value is a string[] and onValueChange receives string[]. The trigger emits one hidden input[name="{name}[]"] per selected value for form submission. A footer with a clear-all button appears in the content automatically.

Searchable + Multiple

Both props compose. SelectContent renders the search input at the top, items filter as the user types, and selected items toggle on click without closing the listbox.

<SelectRoot multiple searchable defaultValue={['design']}>
  <SelectTrigger>
    <SelectValue placeholder="Pick roles…" />
  </SelectTrigger>
  <SelectContent searchPlaceholder="Filter roles…" searchEmpty="No matching roles">
    {ROLES.map(([v, label]) => (
      <SelectItem key={v} value={v}>{label}</SelectItem>
    ))}
  </SelectContent>
</SelectRoot>

Props — SelectRoot

PropTypeDefaultDescription
multiplebooleanToggle multi-select. Items toggle on click; the listbox stays open until dismissed.
valuestring | string[] | nullControlled value. `string` (or `null`) when single; `string[]` when `multiple` is on.
defaultValuestring | string[]Uncontrolled initial value.
onValueChange(value: string | string[]) => voidFires on selection change. Type matches the `multiple` mode.
searchablebooleanRender a filter input at the top of the listbox.
searchPlaceholderstring'Search…'Placeholder text for the search input.
open / defaultOpen / onOpenChangeboolean / (open) => voidStandard open-state plumbing.
disabledbooleanDisable the entire select.
namestringEmit hidden input(s) for form submission.

Props — SelectValue

PropTypeDefaultDescription
placeholderReactNodeShown when no value is selected.
formatMultiple(labels: ReactNode[], values: string[]) => ReactNodeCustom multi-select renderer. Default: labels joined with ", " (e.g. "Design, Frontend").

Props — SelectItem

PropTypeDefaultDescription
valuestringRequired.
disabledbooleanSkipped in keyboard nav, not selectable.
searchTextstringOverride the search-filter text when children are JSX. Default: string children.

Anatomy

PropTypeDefaultDescription
SelectRootComponentWraps the entire select. Accepts value/defaultValue/onValueChange + open/defaultOpen/onOpenChange + multiple/searchable.
SelectTriggerComponentThe visible control. Renders the value plus a chevron.
SelectValueComponentRenders the current value(s) or `placeholder` when none.
SelectContentComponentThe dropdown panel. Portaled to the body. Renders search + footer when applicable.
SelectItemComponentOne option. Requires `value` prop.