Multi Select
The MultiSelect component lets users pick one or more options from a searchable dropdown.
- It uses a
value/onChangeAPI consistent with the rest of the library. - Supports multiple sizes, disabled state, validation messages.
- Supports an optional max-selection limit.
- Supports built-in search and select-all toggle, which can be turned off if not needed.
- Supports custom extra toggles (e.g. show deprecated) with
toggleLabelandonToggleprops.
Import
import { MultiSelect } from "h2o-library";
// Optional, if you want to type your options:
import type { MultiSelectOption } from "h2o-library/types";Usage
Basic
const [selected, setSelected] = useState<MultiSelectOption[]>([]);
<MultiSelect
label="Specialties"
options={[
{ label: "Cardiology", value: "cardiology" },
{ label: "Neurology", value: "neurology" },
]}
value={selected}
onChange={setSelected}
placeholder="Select specialties"
showClear
/>;Sizes
<MultiSelect size="sm" label="Small" options={options} value={value} onChange={setValue} />
<MultiSelect size="md" label="Medium" options={options} value={value} onChange={setValue} />
<MultiSelect size="lg" label="Large" options={options} value={value} onChange={setValue} />
With Max Selections
<MultiSelect
label="Pick up to 3"
options={options}
value={selected}
onChange={setSelected}
maxSelectedOptions={3}
note="Maximum 3 selections."
/>;Selecting all with the built-in toggle also respects
maxSelectedOptions.
Without Search or Select-All
<MultiSelect
options={options}
value={selected}
onChange={setSelected}
showSearch={false}
showSelectAll={false}
/>;With Custom Toggle
The toggle can be used for any boolean state relevant to the options (e.g. show deprecated, include archived, etc.) and is independent of the selected values.
<MultiSelect
options={options}
value={selected}
onChange={setSelected}
toggleLabel="Show deprecated"
onToggle={(checked) => console.log("Show deprecated:", checked)}
/>;Validation States
The component supports error and note states, which display messages below the trigger. The error state is styled with red text and takes precedence over the note if both are provided.
<MultiSelect
label="Departments"
options={options}
value={selected}
onChange={setSelected}
error="Please select at least one department."
/>
<MultiSelect
label="Departments"
options={options}
value={selected}
onChange={setSelected}
note="Select all departments this team covers."
/>
Disabled
<MultiSelect
label="Assigned specialties"
options={options}
value={preselected}
onChange={() => {}}
disabled
/>;Playground
Usage in Forms
Controlled with useState
const [departments, setDepartments] = useState<MultiSelectOption[]>([]);
const [error, setError] = useState("");
function handleSubmit() {
if (departments.length === 0) {
setError("Please select at least one department.");
return;
}
setError("");
// submit departments.map((d) => d.value)
}
<MultiSelect
label="Departments"
options={departmentOptions}
value={departments}
onChange={(v) => {
setDepartments(v);
setError("");
}}
error={error}
showClear
/>;With react-hook-form (Controller)
import { useForm, Controller } from "react-hook-form";
import type { MultiSelectOption } from "h2o-library/types";
type FormValues = { specialties: MultiSelectOption[] };
function PatientForm() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
defaultValues: { specialties: [] },
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="specialties"
control={control}
rules={{
validate: (v) => v.length > 0 || "Select at least one specialty.",
}}
render={({ field }) => (
<MultiSelect
label="Specialties"
options={specialtyOptions}
value={field.value}
onChange={field.onChange}
error={errors.specialties?.message}
showClear
/>
)}
/>
<Button type="submit" label="Save" />
</form>
);
}Extracting values for submission
// The onChange gives you full option objects; extract values for your API:
const payload = {
specialtyIds: selected.map((opt) => opt.value),
};Keyboard
When the role="combobox" trigger is focused and the field is not disabled:
| Key | Action |
|---|---|
Enter / Space | Opens the panel if closed, or closes it if open. |
Escape | Closes the panel and clears search text when open. |
Inside the panel, Tab moves through the search field (if shown), select-all / toggle controls, and each option’s Checkbox (native Space toggles). Arrow keys do not move between options by default.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options* | MultiSelectOption[] | — | The full list of available options. |
value* | MultiSelectOption[] | — | Currently selected options (controlled). |
onChange* | (options: MultiSelectOption[]) => void | — | Called when the selection changes. |
label | string | undefined | Label displayed above the trigger. |
placeholder | string | undefined | Text shown when nothing is selected. |
size | "sm" | "md" | "lg" | "md" | Height of the trigger row. |
disabled | boolean | false | Disables the component. |
maxSelectedOptions | number | undefined | Prevents selecting more than this many options. |
showClear | boolean | false | Shows a clear button when options are selected. |
showSearch | boolean | true | Show a search input inside the dropdown. |
showSelectAll | boolean | true | Show a select-all toggle at the top of the list. |
toggleLabel | string | undefined | Label for a custom extra toggle (e.g. show deprecated). |
onToggle | (checked: boolean) => void | undefined | Fired when the custom toggle changes. |
error | string | undefined | Error message displayed below the component. |
note | string | undefined | Helper text displayed below the component. |
className | string | undefined | Additional CSS classes for the trigger row. |
containerClassName | string | undefined | Additional CSS classes for the outer container. |
* Required
MultiSelectOption
| Prop | Type | Default | Description |
|---|---|---|---|
label* | string | — | Display text. |
value* | string | — | Unique identifier. |
* Required
Design Guidelines
- Use
MultiSelectwhen the selected items list should be visible below the trigger and a select-all toggle is useful. - Prefer
CompactMultiSelectwhen space is constrained and selected values should stay visible inside the trigger as chips. - Set
maxSelectedOptionswhen the domain has a natural upper limit. - Extract
option.valuearrays when sending data to an API — store the full option objects only in component state.
Accessibility
- Each option renders a
Checkboxreachable via Tab. - The search input is focused when the dropdown opens.
- Selected state is visually distinguished with a checkmark.
- has all aria attributes for screen readers (e.g.
aria-selectedon options,aria-controlsfrom trigger to list, etc.).