ComboboxAsync
v1.0.23Updated 20 Oct 2026Async-loading combobox with debounced fetch, optional creatable entries, multi-select with chips, free-solo input, and grouped results. Exports ComboboxAsync + ComboboxAsyncMulti.
Preview
Live · 10 stories▸Why this is recommendedcomboboxautocompleteasyncserver-search+4
ComboboxAsync loading its data asynchronously.
- ✓Customer or supplier lookup against a server API
- ✓Searching a product catalogue too large to preload
- ✓Owner / assignee pickers backed by a user-search endpoint
- ✓Address or place autocomplete
- ✓Pick-or-create a customer while drafting an invoice
- ✓Any picker over thousands of records that must be queried per keystroke
- Host-driven async loading via onSearchChange + options
- Loading spinner and error panel with a Retry button
- Creatable: "Create '<query>'" row when there is no exact match
- Free-solo: commit arbitrary typed text as the value
- Grouped results with headings plus a meta line per option
- Enter commits the create / free-solo row
- A selected value keeps its label after the result set moves on — the chosen option is remembered, and `selectedOption` covers a value that was never in a result set
- Every reset of the internal search is announced through `onSearchChange`, and `onOpenChange` reports open and close
- Custom trigger via the `trigger` prop
- Labelled by FormField: id and aria-* land on the role=combobox trigger, which then fills the field column
- ✕Static option sets already in memory (use Combobox)
- ✕Selecting multiple values (use ComboboxAsyncMulti)
- ✕Plain free-text entry with no suggestions (use Input)
- ✕Command palettes and action menus (use Command)
Install
Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.
Usage
Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.
Props
optionsCurrent result set — the host replaces it in response to onSearchChange
yesonSearchChangeFires on every keystroke; the host should debounce and fetch. Supplying it also turns off the built-in cmdk filter. Also fires with an empty string whenever the component clears its own search (after a selection, a create or a free-solo commit), so the host never keeps a query the user can no longer see
—onOpenChangeFires on every open and close of the popover, including the programmatic close after a selection — lets the host drop its query when the box closes without one
—valueSelected option value (or raw text when freeSolo)
—onValueChangeFires on select; re-selecting the current value clears it
—selectedOptionThe option the current value stands for. The trigger falls back to its label when the value is not in the current options — needed only for a value the component has never seen, such as a form that mounts with a saved value. Ignored unless its value matches the current value, and the current options always win
—loadingShow the loading spinner in the list
—errorError message; replaces the list with an error panel
—onRetryShows a Retry button inside the error panel
—creatableShow a "Create '<query>'" row when no option matches exactly
—onCreateCalled with the query on create; the host owns value assignment. Without it, the query is committed as the value
—freeSoloAllow committing any typed text as the value
—placeholderTrigger text when nothing is selected (default "Select…")
—triggerReplace the default trigger button; the forwarded ref stays on the default trigger
—disabledDisable the trigger
—scrollbarHow the option list draws its scrollbar once it overflows — forwarded to CommandList. Default 'thin': a visible themed bar, because the height cap is the only thing hiding the remaining options
—listMaxHeightCap the option list's height (number = px, string = any CSS length such as '50vh'). Omitted, the list keeps CommandList's 18rem default
—idDOM id set on the trigger — a <label htmlFor> (or FormField) names the control through it
—aria-labelAccessible name for the trigger when there is no visible label
—aria-labelledbyId(s) of the visible label element(s) that name the trigger
—aria-describedbyId(s) of hint or error text that describe the trigger
—aria-invalidMark the trigger invalid — FormField clones this in from its error prop
—aria-requiredMark the trigger required — FormField clones this in from its required prop
—invalidMark the field invalid (destructive ring); aria-invalid wins when both are set
—requiredMark the field required (aria-required on the trigger)
—classNameClass applied to the trigger; a width here wins over the default (w-full inside a FormField, the standalone width otherwise)
—