Select
A form select with a custom trigger, searchable dropdown, and multiselect support — backed by a hidden native `<select>` for full form-library compatibility.
Overview
Select renders a button-style trigger plus a searchable option list, while keeping a visually hidden native <select> in sync underneath — so it works with native form submission, required validation, and libraries like React Hook Form or Formik without extra wiring.
The option list itself is rendered by SelectDropdown, the lower-level primitive that owns search filtering, keyboard focus, and single/multiselect item rendering. Reach for SelectDropdown directly only if you need a dropdown list detached from a trigger button; for a standard form field, use Select.
Import
import { Select } from '@unflow.io/ui/components/Select';Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectDropdownItem[] | — | Required. { label, value, description?, startIcon?, className? }[] rendered as rows |
value | string | string[] | — | Controlled selected value(s). Use a string[] when multiselect |
defaultValue | string | string[] | — | Uncontrolled initial value(s) |
onChange | (event: ChangeEvent<HTMLSelectElement>) => void | — | Fired on the hidden native <select> on every change — read event.target.value (or .selectedOptions for multiselect) |
multiselect | boolean | false | Allows selecting multiple options, shown as removable tags under the trigger |
label / hint | string | — | Field label and helper text, rendered via InputLabel |
placeholder | string | i18n select.placeholder | Trigger text shown while nothing is selected |
required / disableRequiredSymbol | boolean | — | Marks the field required; the second hides the "*" marker |
disabled | boolean | false | Disables the trigger, native select, and tag removal |
fullWidth | boolean | — | Trigger and tag row span 100% of the container width |
status / statusMessage | 'valid' | 'invalid' | 'error', string | — | Validation state and message shown inline. Falls back to 'error' automatically when the native select fails HTML validation (e.g. required) |
name | string | — | name of the hidden native <select>, used for form submission |
defaultOpen | boolean | false | Dropdown open on initial mount |
keepOpen | boolean | — | Keeps the dropdown open after picking an option (single select only — multiselect always stays open until click-away) |
disableSearch | boolean | — | Hides the search input inside the dropdown |
controlledSearch | string | — | Controlled search query — pair with onSearchChange |
onSearchChange / onSearchClear | (event) => void | — | Fired when the search input changes / its clear button is pressed |
maxVisibleRows | number | 5 | Number of option rows visible before the list scrolls |
rowHeight | number | — | Fixed row height in px |
actions | IActionCard[] | — | Extra actions rendered below the option list (e.g. "Clear selection") |
Basic usage
const [value, setValue] = useState('');
<Select
name="fruit"
label="Favorite fruit"
placeholder="Choose a fruit"
value={value}
onChange={(e) => setValue(e.target.value)}
options={[
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
{ label: 'Dragon fruit', value: 'dragon-fruit' },
{ label: 'Elderberry', value: 'elderberry' },
]}
/>Multiselect
Selected options render as removable tags below the trigger. value/onChange deal in string[] while multiselect is set.
const [value, setValue] = useState<string[]>([]);
<Select
name="assignees"
label="Assignees"
placeholder="Assign teammates"
multiselect
value={value}
onChange={(e) => setValue(Array.from(e.target.selectedOptions, (o) => o.value))}
options={[
{ label: 'Ana Silva', value: 'ana' },
{ label: 'Bruno Costa', value: 'bruno' },
{ label: 'Carla Dias', value: 'carla' },
{ label: 'Diego Alves', value: 'diego' },
]}
/>With icons
Each option can carry a startIcon, shown both in the option row and next to the trigger label once selected.
const [value, setValue] = useState('cloud');
<Select
name="integration"
label="Integration"
value={value}
onChange={(e) => setValue(e.target.value)}
options={[
{ label: 'Cloud sync', value: 'cloud', startIcon: <Cloud size={14} /> },
{ label: 'Favorites', value: 'favorites', startIcon: <Heart size={14} /> },
]}
/>Validation status
Pass status/statusMessage to surface inline validation feedback. The same error state is applied automatically when the hidden native select fails HTML validation (e.g. a required field submitted empty).
<Select
name="plan"
label="Plan"
required
status="error"
statusMessage="Please select a plan to continue."
options={[
{ label: 'Starter', value: 'starter' },
{ label: 'Pro', value: 'pro' },
{ label: 'Enterprise', value: 'enterprise' },
]}
/>Disabled
<Select
name="region"
label="Region"
disabled
defaultValue="eu"
options={[
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
]}
/>Customization (slots & slotProps)
| Slot | Type | Description |
|---|---|---|
triggerIcon | ReactNode | Overrides the caret icon on the trigger |
tag | (option: SelectDropdownItem) => ReactNode | Custom render for each selected multiselect tag |
| Slot Prop | Props | Description |
|---|---|---|
trigger | Omit<BaseButtonProps, ...> & { startEl?: ReactNode } | The trigger <button>; startEl overrides the leading icon |
triggerLabel | BaseHTMLProps<HTMLParagraphElement> | The trigger's label text |
triggerStartElRoot / triggerIconRoot | BaseHTMLProps<HTMLDivElement> | Wrapper divs for the leading icon / caret icon |
status | Omit<IInputStatus, 'status' | 'message'> | The inline validation indicator |
tagsRoot / tag | BaseHTMLProps<HTMLDivElement> / Omit<ITag, 'label'> | Tags row wrapper / each selected-option tag (multiselect) |
dropdown | Omit<ISelectDropdown, ...> | Forwarded to the underlying SelectDropdown — search input, list, and actions rendering |
inputLabel | Omit<IInputLabel, ...> | Forwarded to the wrapping InputLabel |
Accessibility
- The trigger exposes
aria-haspopup="listbox",aria-expanded, andaria-controlspointing at the dropdown's listbox, plusaria-invalid/aria-errormessagewhen a validation message is shown. - ArrowUp/ArrowDown on the closed trigger opens the dropdown, matching native
<select>keyboard behavior. - The hidden native
<select>mirrors every selection change (dispatching realinput/changeevents), so browser validation, autofill, and form libraries all see a standard select element.