Combobox
A single-value select with an editable input. The option list filters as the user types — by default with the built-in fuzzy matcher shared with CommandPalette, or with a custom filterFn. Reach for Combobox when the option set is too large for a plain Select, when the user might type a value that isn’t there (freeSolo), or when they should be able to add new entries on the fly (creatable).
Live Preview
When to use
- Combobox — long lists where typing is faster than scrolling, or any flow where the user might type a value not in the list.
- Select — short, fixed lists where the value must come from a known set. Lighter weight.
- MultiSelect — same shape as
Comboboxbut the user picks several values at once.
Import
import { Combobox } from 'entangle-ui';import type { ComboboxOption } from 'entangle-ui';Usage
const options: ComboboxOption[] = [ { value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }, { value: 'svelte', label: 'Svelte' },];
<Combobox label="Framework" options={options} value={framework} onChange={setFramework}/>;Basic
Controlled vs uncontrolled
Both modes are supported. onChange receives null when the value is cleared.
// Controlledconst [value, setValue] = useState<string | null>(null);<Combobox options={options} value={value} onChange={setValue} />
// Uncontrolled<Combobox options={options} defaultValue="react" />Sizes
Sizes
<Combobox size="sm" options={options} /><Combobox size="md" options={options} /><Combobox size="lg" options={options} />Variants
Variants
<Combobox variant="default" options={options} /><Combobox variant="ghost" options={options} /><Combobox variant="filled" options={options} />Free-solo mode
freeSolo lets the user commit an arbitrary string that isn’t in the options list. The onChange handler fires with the typed value on Enter or blur.
Free-solo
<Combobox freeSolo label="Tag" options={existingTags} value={tag} onChange={setTag}/>Creatable
creatable surfaces a Create "<query>" row when the query has no exact match. Pressing Enter (or clicking the row) invokes onCreate with the typed string — the consumer is responsible for adding the new option to the list. Implies freeSolo.
Creatable
const [options, setOptions] = useState(initialOptions);const [value, setValue] = useState<string | null>(null);
<Combobox creatable options={options} value={value} onChange={setValue} onCreate={input => { const created = { value: input, label: input }; setOptions(prev => [...prev, created]); setValue(input); }}/>;Customise the row’s label via createLabel:
<Combobox creatable createLabel={q => ( <> Add new tag: <strong>{q}</strong> </> )} options={options} onCreate={handleCreate}/>Loading
Async option sources hand the component a loading flag — the dropdown shows a spinner row instead of options. Pair with onInputChange to debounce a remote search and re-feed options once the response lands.
Loading
<Combobox options={options} loading={isFetching} loadingMessage="Searching..." onInputChange={query => debouncedSearch(query)}/>Open on focus
By default the dropdown opens once the user starts typing. openOnFocus opens it the moment the input receives focus — useful for short curated lists where the input is more of a “filterable picker”.
Open on focus
<Combobox openOnFocus options={options} placeholder="Click to browse" />Custom filter
Replace the built-in fuzzy matcher with your own logic — filterFn can return a boolean (keep / drop) or a numeric score (higher = ranked higher).
<Combobox options={options} filterFn={(option, query) => option.value.startsWith(query.toLowerCase()) ? 2 : 0 }/>Error state
Error
<Combobox label="Country" options={options} error errorMessage="Pick a country to continue" required/>Form integration
Use name to render a hidden <input> with the selected value for native form submission.
<Combobox name="country" options={countries} value={country} onChange={setCountry}/>Accessibility
- Input renders with
role="combobox",aria-haspopup="listbox", andaria-expandedreflecting the dropdown state. - Dropdown is a
role="listbox"withrole="option"children; the active option is reflected viaaria-activedescendant. - Keyboard: Arrow Up / Down to navigate, Enter to select (or create in
creatablemode), Escape to close, Home / End for first / last option, Backspace on an empty input clears a selected value whenclearable. aria-invalidis set in the error state;aria-requiredwhenrequiredis set.aria-describedbylinks to the helper / error text.- Clear button (when shown) has
aria-label="Clear selection".
API Reference
<Combobox>
| Prop | Type | Default | Description |
|---|---|---|---|
value | T | null | — | Controlled selected value. |
defaultValue | T | — | Default selected value (uncontrolled). |
options | ComboboxOption<T>[] | — | Available options. |
placeholder | string | — | Placeholder rendered when the input is empty. |
filterFn | (option: ComboboxOption<T>, query: string) => boolean | number | — | Custom filter. Boolean keeps / drops; number is a score (higher = ranked higher). Defaults to the built-in fuzzy matcher. |
emptyMessage | string | 'No results found' | Message shown when filtering yields no results. |
loading | boolean | false | Loading indicator — replaces options with a spinner row. |
loadingMessage | string | 'Loading...' | Message shown while loading. |
freeSolo | boolean | false | Allow values not present in `options`. `onChange` fires with the typed string on blur / Enter. |
creatable | boolean | false | Surface a "Create <query>" row when the query has no exact match. Implies `freeSolo`. |
createLabel | (query: string) => ReactNode | — | Render the create row label. Defaults to `Create "<query>"`. |
openOnFocus | boolean | false | Open the dropdown automatically when the input gains focus, even with no query. |
size | 'sm' | 'md' | 'lg' | 'md' | Component size. |
variant | 'default' | 'ghost' | 'filled' | 'default' | Visual variant. |
label | string | — | Label rendered above the input. |
helperText | string | — | Helper text rendered below. |
error | boolean | false | Error state. |
errorMessage | string | — | Error message — overrides `helperText` when `error` is true. |
disabled | boolean | false | Disable the input. |
required | boolean | false | Mark the field as required. |
readOnly | boolean | false | Read-only — input value is fixed. |
clearable | boolean | false | Show a clear button when a value is selected. |
maxDropdownHeight | number | 240 | Maximum dropdown height in pixels. |
minDropdownWidth | number | — | Minimum dropdown width in pixels. |
name | string | — | Hidden form-input name. |
onChange | (value: T | null) => void | — | Selection change handler. `null` is passed when cleared. |
onInputChange | (input: string) => void | — | Free-text change handler — fires every keystroke regardless of selection. |
onCreate | (input: string) => void | — | Called when the user creates a new option (only with `creatable`). Falls back to `onChange` when omitted. |
onOpenChange | (open: boolean) => void | — | Open state change handler. |
className | string | — | Additional CSS class names. |
style | CSSProperties | — | Inline styles. |
testId | string | — | Test identifier for automated testing. |
ref | Ref<HTMLInputElement> | — | Ref to the underlying input element. |
ComboboxOption
| Prop | Type | Default | Description |
|---|---|---|---|
value * | T | — | Unique value used for selection. |
label | string | — | Display label. Falls back to `value` when omitted. |
description | string | — | Optional description rendered next to the label. |
icon | ReactNode | — | Optional icon rendered before the label. |
disabled | boolean | — | Whether this option is disabled. |