Multi Select

The MultiSelect component lets users pick one or more options from a searchable dropdown.

  • It uses a value/onChange API 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 toggleLabel and onToggle props.

Import

import { MultiSelect } from "h2o-library";
// Optional, if you want to type your options:
import type { MultiSelectOption } from "h2o-library/types";

Usage

Basic

Basic MultiSelect
With Select-All Toggle
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

Specialties
Specialties
Specialties
<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

With Max Selections (3)
<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.

With Custom Toggle
<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.

With Validation
Please select at least one specialty.
With Note
<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

Loading 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:

KeyAction
Enter / SpaceOpens the panel if closed, or closes it if open.
EscapeCloses 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

PropTypeDefaultDescription
options*MultiSelectOption[]The full list of available options.
value*MultiSelectOption[]Currently selected options (controlled).
onChange*(options: MultiSelectOption[]) => voidCalled when the selection changes.
labelstringundefinedLabel displayed above the trigger.
placeholderstringundefinedText shown when nothing is selected.
size"sm" | "md" | "lg""md"Height of the trigger row.
disabledbooleanfalseDisables the component.
maxSelectedOptionsnumberundefinedPrevents selecting more than this many options.
showClearbooleanfalseShows a clear button when options are selected.
showSearchbooleantrueShow a search input inside the dropdown.
showSelectAllbooleantrueShow a select-all toggle at the top of the list.
toggleLabelstringundefinedLabel for a custom extra toggle (e.g. show deprecated).
onToggle(checked: boolean) => voidundefinedFired when the custom toggle changes.
errorstringundefinedError message displayed below the component.
notestringundefinedHelper text displayed below the component.
classNamestringundefinedAdditional CSS classes for the trigger row.
containerClassNamestringundefinedAdditional CSS classes for the outer container.

* Required

MultiSelectOption

PropTypeDefaultDescription
label*stringDisplay text.
value*stringUnique identifier.

* Required

Design Guidelines

  • Use MultiSelect when the selected items list should be visible below the trigger and a select-all toggle is useful.
  • Prefer CompactMultiSelect when space is constrained and selected values should stay visible inside the trigger as chips.
  • Set maxSelectedOptions when the domain has a natural upper limit.
  • Extract option.value arrays when sending data to an API — store the full option objects only in component state.

Accessibility

  • Each option renders a Checkbox reachable 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-selected on options, aria-controls from trigger to list, etc.).