Browse docs
Browse docs
A combobox. Single or multi-select. Sync from a static options array or async via loadOptions(query). Free-solo mode lets the user commit arbitrary text. Generic typing — your option shape stays whatever you need it to be.
The implementation is deliberately custom-built in pure React (no react-aria-components) — the F5-A-bis rewrite chose deterministic state ownership over headless inheritance because the clear-button regression was a motivating lesson.
import { Autocomplete } from '@dashforge/tw';
<Autocomplete
name="city"
label="City"
options={[
{ value: 'rome', label: 'Rome' },
{ value: 'milan', label: 'Milan' },
{ value: 'paris', label: 'Paris' },
]}
/>import { DashForm } from '@dashforge/forms';
import { Autocomplete, Button } from '@dashforge/tw';
<DashForm onSubmit={onSubmit}>
<Autocomplete
name="country"
label="Country"
options={COUNTRIES}
placeholder="Pick one"
required
/>
<Button type="submit" color="primary">Continue</Button>
</DashForm>import { Autocomplete } from '@dashforge/tw';
const COUNTRIES = [
{ value: 'it', label: 'Italy' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'es', label: 'Spain' },
// …
];
<Autocomplete
name="country"
label="Country"
placeholder="Type to search…"
options={COUNTRIES}
/>import { Autocomplete } from '@dashforge/tw';
const TAGS = [
{ value: 'frontend', label: 'Frontend' },
{ value: 'backend', label: 'Backend' },
{ value: 'devops', label: 'DevOps' },
{ value: 'design', label: 'Design' },
// …
];
<Autocomplete
name="tags"
label="Tags"
placeholder="Pick one or more…"
multiple
options={TAGS}
defaultValue={['frontend', 'design']}
/><Autocomplete
name="tags"
label="Tags"
options={TAGS}
multiple
placeholder="Add tags…"
/>In multiple mode the bridge value is string[]. Selected options render as removable chips inside the input.
<Autocomplete
name="role"
label="Job title"
options={SUGGESTIONS}
freeSolo
placeholder="Start typing or pick from suggestions"
/>freeSolo lets the user submit text that doesn't match any option. Enter or blur commits the typed value. Use for "tag" fields where suggestions are hints, not constraints.
<Autocomplete
name="user"
label="Assignee"
loadOptions={async (query) => {
const users = await fetch(`/api/users?q=${query}`).then(r => r.json());
return users.map(u => ({ value: u.id, label: u.name }));
}}
loadDebounceMs={300}
emptyMessage="No users found"
/>loadOptions runs on every keystroke (debounced by loadDebounceMs, default 250). Loading states render via the listBox slot. The pattern is race-safe — out-of-order responses are dropped.
options is generic. Pass any shape, derive value + label:
type User = { id: number; firstName: string; lastName: string; email: string };
<Autocomplete<User>
name="assignee"
options={users}
getOptionValue={(u) => String(u.id)}
getOptionLabel={(u) => `${u.firstName} ${u.lastName} (${u.email})`}
getOptionDisabled={(u) => u.email.endsWith('@disabled.com')}
/><Autocomplete
name="emails"
label="Recipients"
options={CONTACTS}
multiple
freeSolo
placeholder="[email protected], or pick from your contacts"
/>Both axes are independent — useful for "email recipients" fields where most are picked from contacts but ad-hoc addresses are allowed.
<Autocomplete size="sm" name="x" options={OPTIONS} />
<Autocomplete size="md" name="x" options={OPTIONS} /> {/* default */}
<Autocomplete size="lg" name="x" options={OPTIONS} />
<Autocomplete layout="inline" name="x" options={OPTIONS} />| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Field name. |
options | TOption[] | — | Sync options array (generic). |
loadOptions | (q: string) => Promise<TOption[]> | — | Async loader. Replaces options. |
loadDebounceMs | number | 250 | Debounce for loadOptions. |
loadingMessage | ReactNode | — | Custom loading row. |
emptyMessage | string | — | Shown when filtered list is empty. |
multiple | boolean | false | Multi-select mode (chips). |
freeSolo | boolean | false | Allow ad-hoc string values. |
getOptionValue | (o: TOption) => string | o.value | Map option to bridge value. |
getOptionLabel | (o: TOption) => string | o.label | Display text. |
getOptionDisabled | (o: TOption) => boolean | — | Per-option disable. |
getOptionKey | (o: TOption) => string | — | Custom React key (default getOptionValue). |
value / defaultValue | string | string[] | null | — | Controlled / uncontrolled. |
onValueChange | (v) => void | — | Change handler. |
placeholder | string | — | Input placeholder. |
label / required / rules / error / disabled / fullWidth | standard | — | TextField-like. |
size | 'sm' | 'md' | 'lg' | 'md' | Density. |
layout | 'stacked' | 'inline' | 'stacked' | Label position. |
visibleWhen / access | reactive + RBAC | — | Same as TextField. |
slotProps | AutocompleteSlotProps | — | Per-slot overrides. |
sx | string | — | Utility-class override. |
root · label · requiredMark · inputWrapper · input · trigger · clearButton · popover · listBox · listItem · emptyState · helperText · errorText · chipsList · chip · chipRemove
loadOptions call gets a monotonically-increasing generation id. Responses from older generations are dropped. So fast typing never paints stale results.string[] (in selection order); single-select is string | null.freeSolo + multiple combine: each commit (Enter / blur) adds a chip; backspace on empty input removes the last chip. Useful for email recipient pickers.options path. For large lists (10k+), prefer loadOptions async with server-side filtering — the listBox doesn't virtualize yet.