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
| Prop | Type | Default | Description |
|---|---|---|---|
multiple | boolean | — | Toggle multi-select. Items toggle on click; the listbox stays open until dismissed. |
value | string | string[] | null | — | Controlled value. `string` (or `null`) when single; `string[]` when `multiple` is on. |
defaultValue | string | string[] | — | Uncontrolled initial value. |
onValueChange | (value: string | string[]) => void | — | Fires on selection change. Type matches the `multiple` mode. |
searchable | boolean | — | Render a filter input at the top of the listbox. |
searchPlaceholder | string | 'Search…' | Placeholder text for the search input. |
open / defaultOpen / onOpenChange | boolean / (open) => void | — | Standard open-state plumbing. |
disabled | boolean | — | Disable the entire select. |
name | string | — | Emit hidden input(s) for form submission. |
Props — SelectValue
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | ReactNode | — | Shown when no value is selected. |
formatMultiple | (labels: ReactNode[], values: string[]) => ReactNode | — | Custom multi-select renderer. Default: labels joined with ", " (e.g. "Design, Frontend"). |
Props — SelectItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. |
disabled | boolean | — | Skipped in keyboard nav, not selectable. |
searchText | string | — | Override the search-filter text when children are JSX. Default: string children. |
Anatomy
| Prop | Type | Default | Description |
|---|---|---|---|
SelectRoot | Component | — | Wraps the entire select. Accepts value/defaultValue/onValueChange + open/defaultOpen/onOpenChange + multiple/searchable. |
SelectTrigger | Component | — | The visible control. Renders the value plus a chevron. |
SelectValue | Component | — | Renders the current value(s) or `placeholder` when none. |
SelectContent | Component | — | The dropdown panel. Portaled to the body. Renders search + footer when applicable. |
SelectItem | Component | — | One option. Requires `value` prop. |