Select
The Select component provides a dropdown for choosing a single value from a list of options. It supports both controlled and uncontrolled modes, clearable selections, async loading, search filtering, grouped options, and the usual form-field affordances (label, caption, error/warning).
Variations
Clearable selection
Set clearable to show a clear button when a value is selected. Clearing the field resets the value to undefined, calls onChange(undefined), and invokes onClear when provided.
Suffix
The Select component can be customized with a suffix element that appears on the right side of the input.
Form integration
The Select component integrates seamlessly with form elements like labels, captions, and validation messages.
Search
Enable client-side search filtering to help users find options quickly in large or structured lists.
Async loading
Options can be loaded asynchronously by passing a function that returns a Promise.
Disabled states
Both the entire select and individual options can be disabled.
Open callback
The onOpen callback is triggered when the dropdown menu opens. This is useful for lazy loading data, logging analytics, or triggering side effects when users interact with the select.
Loading states
The Select component provides clear loading indicators for different scenarios.
Complex option labels
Options can have ReactNode labels for rich content display.
Custom option renderer
Customize how individual options are rendered using the optionRenderer prop. This allows advanced layouts or UI elements for each option.
Grouped options and separators
Header and footer
Sizes
The Select component supports two sizes: md (default) and sm. The default size is suitable for most use cases, while the sm size is useful for compact layouts.
With tooltip
tooltipContent is used to display the content of the tooltip. tooltipProps is used to pass additional props to the tooltip.
Orientation
The Select component accepts an optional orientation prop, which defines the layout direction of the input and its label.
Available options are horizontal and vertical (default).
Installation
import { Select } from "@harnessio/ui/components";
//...
return ( <Select options={[ { label: "One", value: "one" }, { label: "Two", value: "two" }, { label: "Three", value: "three" }, { label: "Four", value: "four", disabled: true }, ]} defaultValue="three" onChange={(value) => console.log(value)} />)Anatomy
Select is a single component driven by the options prop — a flat list, grouped lists, or an async function that resolves options. Control the selection with value + onChange, or leave it uncontrolled with defaultValue.
<Select options={[ { label: "One", value: "one" }, { label: "Two", value: "two" }, ]} value={value} onChange={setValue} placeholder="Select an option"/>- Options can be flat (
{ label, value, disabled? }), grouped ({ label, options: [...] }), or a separator (the string'-'). Pass a function returning aPromisefor async loading; the component shows a spinner while it resolves. - Search — set
allowSearchto show a search box in the dropdown; the default filter matches the option label.onSearchis a notification callback fired with the (debounced) query — it doesn’t replace the built-in filtering. - Form field —
label,caption,error, andwarningrender the surrounding form chrome; a hidden input carries thename/valuefor native form submission. - Dropdown content — customize with
header,footer,optionRenderer, andcontentWidth;onScrollEndfires near the bottom for infinite scroll andonOpenfires when the menu opens.
It’s built on the DropdownMenu primitive rather than a native <select>, so the trigger opens a menu of items rather than a native option list.
Best practices
Do
- Use
Selectfor choosing one value from a moderate-to-long list; for a few always-visible options, preferRadio. - Turn on
allowSearchonce the list gets long enough that scanning is slow. - Set a
placeholderthat names what’s being chosen (“Select a branch”), and preselect a sensibledefaultValuewhen one exists.
Don’t
- Don’t use
error/warningdecoratively — they force the danger/warning theme and read as validation feedback. - Don’t nest deep group hierarchies; grouping is a single level of labelled sections with optional separators.
Content
- Write option labels as short, parallel phrases; reserve rich
ReactNodelabels for cases that genuinely need an icon or secondary text. - Keep group labels (“Fruits”, “Vegetables”) as plain category names.
Accessibility
Keyboard
- The trigger is a button that opens a menu (via the
DropdownMenuprimitive): arrow keys move between items,Enterselects, andEscapecloses. WhenallowSearchis on, typing in the search box narrows the list and the arrow keys still move the highlight through the filtered items.
Labeling
- Provide a
label(or associate your own) so the trigger has an accessible name; the label is wired to the control viahtmlFor/id, and anidis generated when you don’t pass one. - The selected option is indicated with a checkmark in the menu.
Focus & states
- Because it’s built on a menu primitive rather than a native
listbox/combobox, it uses menu semantics rather than the native selected-option model.errorandwarningdrive the visual theme; wire them from your own validation.
Properties
Select
Prop | Required | Default | Type |
|---|---|---|---|
| options | true | SelectOption<T>[] | (() => Promise<SelectOption<T>[]>) | |
| value | false | T | |
| defaultValue | false | T | |
| onChange | false | (value: T | undefined) => void | |
| clearable | false | false | boolean |
| onClear | false | () => void | |
| onOpen | false | () => void | Promise<void> | |
| onScrollEnd | false | () => void | |
| isLoading | false | false | boolean |
| allowSearch | false | false | boolean |
| onSearch | false | (query: string) => void | |
| searchValue | false | string | |
| optionRenderer | false | (option: ValueOption<T>) => SelectItemType | |
| size | false | 'md' | 'md' | 'sm' |
| placeholder | false | 'Select an option' | string |
| theme | false | 'default' | 'default' | 'danger' | 'warning' |
| label | false | string | |
| caption | false | string | |
| error | false | string | |
| warning | false | string | |
| optional | false | boolean | |
| orientation | false | 'vertical' | 'vertical' | 'horizontal' |
| tooltipContent | false | Exclude<ReactNode, boolean | null | undefined> | |
| tooltipProps | false | IconWithTooltipProps | |
| labelSuffix | false | ReactNode | |
| prefix | false | ReactNode | |
| suffix | false | ReactNode | |
| header | false | ReactNode | |
| footer | false | ReactNode | |
| contentWidth | false | 'auto' | 'auto' | 'triggerWidth' |
| disabled | false | boolean | |
| id | false | string | |
| name | false | string | |
| wrapperClassName | false | string | |
| triggerClassName | false | string | |
| contentClassName | false | string |
SelectOption
Each option is a value option, a group of value options, or a separator ('-').
type SelectOption<T = string> = | { label: string | ReactNode; value: T; disabled?: boolean; } | { label: string; options: Array< | { label: string | ReactNode; value: T; disabled?: boolean; } | "-" >; } | "-";Prop | Required | Default | Type |
|---|---|---|---|
| label | true | string | ReactNode | |
| value | true | T | |
| disabled | false | false | boolean |