CustomSelect
Fully custom-styled select with optional search, icons, descriptions and keyboard support.
A fully custom-styled dropdown select with optional in-dropdown search, per-option icons and descriptions, and complete keyboard support (arrows, Home/End, Enter, Escape). It's controlled: pass value and onChange, plus an options array. For free-text entry or creatable values see Combobox; for the Radix-based composed parts see Select.
Import
1import { CustomSelect } from "@yuhuanowo/yunui";Usage
CustomSelect is controlled — wire value and onChange to state:
1const [value, setValue] = useState("medium");2 3<CustomSelect4 value={value}5 onChange={setValue}6 searchable7 placeholder="Select a model size"8 options={[9 { value: "small", label: "Small", description: "Fast, lower cost" },10 { value: "medium", label: "Medium", description: "Balanced" },11 { value: "large", label: "Large", description: "Highest quality" },12 { value: "xl", label: "Extra Large", description: "Maximum capacity" },13 ]}14/>Example
Set searchable to add a filter box inside the dropdown. Each option can carry an icon and a secondary description line.
Searchable select with descriptions
Server-backed search
By default searchable filters the given options client-side. Pass onSearch to drive results from a backend instead: it fires (debounced by searchDebounceMs, default 250) with the current query, and you replace options with the fetched results. Pair it with loading to surface the in-flight fetch.
1const [options, setOptions] = useState<CustomSelectOption[]>([]);2const [loading, setLoading] = useState(false);3 4<CustomSelect5 searchable6 value={value}7 onChange={setValue}8 options={options}9 loading={loading}10 onSearch={async (query) => {11 setLoading(true);12 setOptions(await searchModels(query));13 setLoading(false);14 }}15/>Infinite scroll
For long or paged result sets, pass onLoadMore + hasMore. onLoadMore fires when the list is scrolled near its end (and no fetch is in flight); append the next page to options. It's independent of onSearch — use either or both.
1<CustomSelect2 value={value}3 onChange={setValue}4 options={options}5 hasMore={hasMore}6 onLoadMore={() => loadNextPage()}7/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | |
| disabled | boolean | false | Disable interaction and dim the control. |
| hasMore | boolean | false | Whether more options can be loaded; gates `onLoadMore` and the footer spinner. |
| loading | boolean | false | In remote mode, show a loading indicator while results are being fetched. |
| onChange* | (value: string) => void | — | Called with the chosen option's value. |
| onLoadMore | (() => void) | — | Infinite scroll. Called when the options list is scrolled near its end and `hasMore` is true (and no fetch is in flight). The host appends the next page to `options`. Independent of `onSearch` — use either or both. |
| onSearch | ((query: string) => void) | — | Server-backed search. When provided, the component switches to remote mode: it stops filtering `options` locally (the host owns filtering) and instead calls `onSearch` — debounced by `searchDebounceMs` — with the current query so the host can fetch matching options. A search box is shown even without `searchable`. Pair with `loading` to surface the in-flight fetch. |
| options* | SelectOption[] | — | The selectable options. |
| placeholder | string | — | Placeholder shown when nothing is selected (falls back to i18n default). |
| searchable | boolean | false | Show an in-dropdown search box that filters options by label/value. |
| searchDebounceMs | number | 250 | Debounce (ms) before `onSearch` fires. |
| value* | string | — | Currently selected value (controlled). |