Dropdown
Dropdown component is a controlled single-value select follows a11y guidelines.
- Supports a
onChangefunction that receivesstring | undefined - Selecting an option passes the value string;clearing always passes
undefined. - The menu auto-repositions based on viewport space, and search auto-focuses when the dropdown opens.
- Supports disabled options, validation states, and an info tooltip on the label.
Import
import { Dropdown } from "h2o-library";
// optional, if you want to type your options
import type { DropdownOption } from "h2o-library/types";Usage
Basic
const [value, setValue] = useState<string | undefined>(undefined);
<Dropdown
label="Department"
options={options}
value={value}
onChange={setValue}
placeholder="Select a department"
/>;Sizes
Multiple sizes are available for the trigger button, matching our Input component sizes.
The menu and search input scale with the trigger.
With Search
The search input auto-focuses when the dropdown opens.
<Dropdown options={options} enableSearch placeholder="Type to filter…" />;Clear Button
Calling onChange with undefined resets the selection. Use undefined (not "") to represent "nothing selected".
Disabled Options
Trough the disabled property on options, you can prevent selection of specific items while still showing them in the list.
Tooltip on Label
You can add a tooltip to the label, which appears on hover of an info icon next to the label text.
Validation States
Use the error prop to show an error message and red styling, and the note prop for neutral helper text.
Show validation messages after form submission or on field blur, not on every change.
Callbacks
onOpen and onClose callbacks allow you to respond to menu state changes, e.g. for analytics or to reset search input.
<Dropdown
options={options}
onOpen={() => console.log("Dropdown opened")}
onClose={() => console.log("Dropdown closed")}
/>;Playground
Usage in Forms
Uncontrolled — not recommended
Dropdown is a controlled component. For native form participation, combine with a hidden <input>.
<form
onSubmit={(e) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
console.log(data.get("department")); // value from hidden input
}}
>
<input type="hidden" name="department" value={department ?? ""} />
<Dropdown
label="Department"
options={options}
value={department}
onChange={setDepartment}
placeholder="Select…"
/>
<Button type="submit" label="Save" />
</form>;Controlled with useState
const [specialty, setSpecialty] = useState<string | undefined>(undefined);
const [error, setError] = useState("");
function handleSubmit() {
if (!specialty) {
setError("Please select a specialty.");
return;
}
setError("");
// submit
}
<Dropdown
label="Specialty"
options={specialtyOptions}
value={specialty}
onChange={(v) => {
setSpecialty(v);
setError("");
}}
error={error}
placeholder="Select specialty"
/>;With react-hook-form (Controller)
Dropdown is not a native input, so use Controller to integrate it.
import { useForm, Controller } from "react-hook-form";
function PatientForm() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<{
specialty: string;
}>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="specialty"
control={control}
rules={{ required: "Please select a specialty." }}
render={({ field }) => (
<Dropdown
label="Specialty"
options={specialtyOptions}
value={field.value}
onChange={(v) => field.onChange(v ?? "")}
error={errors.specialty?.message}
placeholder="Select specialty"
/>
)}
/>
<Button type="submit" label="Save patient" />
</form>
);
}Keyboard
While focus is inside the dropdown container (trigger, clear control, search field when open, or options):
| Key | Action |
|---|---|
Escape | Closes an open menu (and clears search when enableSearch is on). |
Enter / Space | Standard button behavior on the trigger (open/close) and each role="option" row (select). |
Tab / Shift+Tab | Move focus through interactive controls in the menu. |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options* | DropdownOption[] | — | List of options to display. |
value | string | undefined | Currently selected value (controlled). |
onChange | (value: string | undefined) => void | — | Called on selection. Passes undefined when cleared. |
label | string | undefined | Label displayed above the dropdown. |
placeholder | string | "Select an option" | Text shown when no value is selected. |
size | "sm" | "md" | "lg" | "md" | Size of the dropdown trigger. |
disabled | boolean | false | Disables the dropdown. |
enableSearch | boolean | false | Adds a search input that auto-focuses when the menu opens. |
showClear | boolean | false | Shows a clear button that calls onChange(undefined). |
direction | "top" | "bottom" | "auto" | "auto" | Forces or auto-detects menu direction. |
tooltip | string | undefined | Tooltip shown on an info icon next to the label. |
tooltipPosition | "top" | "bottom" | "left" | "right" | "top" | Position of the tooltip. |
error | string | undefined | Error message displayed below the dropdown. |
note | string | undefined | Helper text displayed below the dropdown. |
onOpen | () => void | undefined | Fired when the menu opens. |
onClose | () => void | undefined | Fired when the menu closes. |
containerClassName | string | undefined | Additional CSS classes for the outer container. |
arrowClassName | string | undefined | Additional CSS classes for the chevron icon. |
* Required
DropdownOption
| Prop | Type | Default | Description |
|---|---|---|---|
label* | string | — | Display text. |
value* | string | — | Unique value identifier. |
disabled | boolean | false | Prevents the option from being selected. |
* Required
Design Guidelines
- Use
Dropdownfor single-value selection from a bounded list (≤ ~50 items without search). - Use
enableSearchfor lists with more than ~8 items. - Use
undefined(not"") to represent "no selection" —onChangealready does this on clear. - Show
errorafter form submission or on field blur, not on every change.
Accessibility
- The trigger is a
<button>witharia-expanded,aria-haspopup="listbox", andaria-controls. - The menu has
role="listbox". Each option hasrole="option"andaria-selected. Escapecloses the menu and returns focus to the trigger.- The label is linked to the trigger via
htmlFor/id.