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} />Configure <Autocomplete> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Autocomplete: {
defaults: { size: 'md', layout: 'stacked', fullWidth: true },
},
},
});Configurable axes (AutocompleteVariantProps):
| Axis | Type | Notes |
|---|---|---|
size | 'sm' | 'md' | 'lg' | Wrapper height + padding + font-size. |
layout | 'stacked' | 'inline' | Label position (above vs left). |
fullWidth | boolean | Stretch root + wrapper to container width. |
Non-visual axes (name, rules, options, loadOptions, getOption*, multiple, freeSolo, label, helperText, error, disabled, access, visibleWhen, onValueChange) are not theme-configurable — per-instance semantics.
Precedence chain (lowest → highest):
defaultVariants from the internal autocompleteVariants recipe.theme.components.Autocomplete.defaults (application-wide).<Autocomplete size="lg" fullWidth />) — wins over theme.sx — appended to the root and wins over conflicting classes via tailwind-merge.TypeScript: theme.components.Autocomplete.defaults autocompletes to the three axes above.
Reactivity: useComponentDefaults('Autocomplete') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Bridge field name (required when used inside DashFormProvider). |
options | TOption[] | — | Selectable options — an array of TOption (defaults to AutocompleteOption). |
access | AccessRequirement | — | RBAC access requirement. |
defaultValue | AutocompleteValue | — | Default value for uncontrolled mode (no-op in form mode). |
disabled | boolean | false | Disables input + list — ORed with RBAC denied:disable. |
emptyMessage | string | — | Fallback text when no option matches the typed filter. |
error | boolean | false | Explicit error semaphore. Overrides the bridge's auto-detected error. |
freeSolo | boolean | — | Allow committing arbitrary strings (not just option values). When true: - Enter commits the typed text as the value when no option row is highlighted (or no rows match the filter). - Blur commits the typed text the same way, unless the text exactly matches an existing option's label (in which case the option's value snaps in). - In multi mode, each free-solo commit adds a new chip whose key and label are the typed string.Free-solo is independent of multiple — works in both modes. |
fullWidth | boolean | false | Stretch root wrapper + input to the container's width. |
getOptionDisabled | (option: TOption) => boolean | (o) => Boolean((o as AutocompleteOption).disabled) | Per-option disabled flag. |
getOptionKey | (option: TOption) => string | — | Unique React key for an option. Defaults to getOptionValue. Override if your value strings can collide across distinct option records (rare). |
getOptionLabel | (option: TOption) => ReactNode | (o) => (o as AutocompleteOption).label | Extract the display label from an option. |
getOptionValue | (option: TOption) => string | (o) => (o as AutocompleteOption).value | Extract the persistable value (string) from an option. |
helperText | ReactNode | — | Helper line below the combobox. Auto-replaced by bridge error when invalid. |
label | ReactNode | — | Visible label above (or left of, per layout) the combobox. |
layout | 'stacked' | 'inline' | 'stacked' | Label placement — 'stacked' (above the combobox) or 'inline' (left of the combobox). |
loadDebounceMs | number | 250 | Debounce window (ms) for loadOptions. The loader fires after the user stops typing for this long. |
loadingMessage | ReactNode | — | Text displayed in the popover while a fetch is in flight. |
loadOptions | (query: string) => Promise<TOption[]> | — | Async option loader. Called (debounced by loadDebounceMs) every time the user types in the input. Results replace the static options prop while the user is actively filtering.Return a promise that resolves with the new option list. Reject / throw to surface a generic empty state. |
multiple | boolean | — | Multi-select mode. When true:- value / defaultValue accept string[] (or null). - Picks are toggled rather than replaced; the popover stays open. - Selected items render as chips inside the input wrapper. - Backspace at the start of an empty input removes the last chip. - The clear button (×) clears the entire selection. |
onValueChange | (value: AutocompleteValue) => void | — | Fires when the user picks an option (after bridge update in form mode). |
placeholder | string | — | Placeholder shown when the input is empty. |
required | boolean | false | Renders the required * marker + sets the native required attribute. |
rules | unknown | — | RHF validation rules — opaque, forwarded to the bridge. |
size | 'md' | 'sm' | 'lg' | 'md' | Density tier — drives wrapper padding + input font-size. |
slotProps | AutocompleteSlotProps | — | Per-slot className overrides. |
sx | string | — | Root className shortcut. |
value | AutocompleteValue | — | Controlled value (form mode reads from the bridge if omitted). |
visibleWhen | (engine: Engine) => boolean | — | Engine predicate — field not rendered when it returns false. |
<Autocomplete> is a compound component with 16 named slots — the richest slot surface in the library. Each slot accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
root | Outer wrapper (label + combobox + helper/error). |
label | The <label> element. |
requiredMark | The red * next to the label when required. |
inputWrapper | The bordered surface around the input + clear/trigger buttons. |
input | The typeable <input> element. |
trigger | The caret button that opens the popover. |
clearButton | The × button that clears the current selection. |
popover | The floating panel holding the option list. |
listBox | The <ul> inside the popover wrapping options. |
listItem | A single option row. |
emptyState | The fallback row shown when the filter has no matches. |
helperText | Helper text line, when not in error state. |
errorText | Helper text line, when in error state. |
chipsList | Container for the selected chips (multi-select mode only). |
chip | An individual selected chip in multi-select. |
chipRemove | The × on a selected chip. |
Use sx for a root-level class override; use slotProps when you need to reach a specific inner element (e.g. popover max-height, listItem hover color, chip background).
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.