Autocomplete Select
Search-first selection field built on Picker for large or grouped option sets.
Overview
AutocompleteSelect is a purpose-built searchable selection field built on top of Picker.
Use it when the main interaction is typing to filter or discover options. It is especially useful when:
- the option set is large
- the list may be grouped
- users should be able to add a new option from the current query
Compared with Select, AutocompleteSelect is more search-centric and more flexible, but also more complex.
Layering
| Component | Role |
|---|---|
Picker | Base primitive |
Select | Standard choose-from-list field |
AutocompleteSelect | Search-first field with grouped-option support |
Basic Usage
<AutocompleteSelect
selected={field.value}
onSelect={field.onChange}
onBlur={field.onBlur}
label="Owning Franchisee Entity"
placeholder="Select a franchisee entity"
labelIcon="Bank"
onListEndReached={
hasNextPage && !isFetchingNextPage ? fetchNextPage : undefined
}
options={
franchiseeGroups?.map((entity) => ({
value: entity.id,
label: entity.name,
})) || []
}
/>This mirrors the franchisee entity field in CreateLocationModal.
Another Standard Field Pattern
The same screen also uses AutocompleteSelect for a simpler local option set:
<AutocompleteSelect
selected={field.value}
onSelect={field.onChange}
onBlur={field.onBlur}
label="Deal Zone"
labelIcon="LocationPinArea"
placeholder="Select a deal zone"
options={
territories?.map((territory) => ({
value: territory.id,
label: territory.name || '',
})) || []
}
/>Use this pattern when the interaction is still search-first, but the option list is already present locally.
Grouped Options
options can also be a grouped record when that improves scanning and discovery.
<AutocompleteSelect
label="Dependency"
options={{
Tasks: taskOptions,
Campaigns: campaignOptions,
Automations: automationOptions,
}}
selected={selectedDependency}
onSelect={setSelectedDependency}
/>Search-Driven Data Updates
Use onChangeSearchText when the backing list should respond to the typed query.
This does not mean AutocompleteSelect should be preferred just because the data source is async. The currently selected option still needs to exist in options, even before the user types, otherwise the closed field can fail to render the current selection correctly.
Treat onChangeSearchText as a search signal. If results are updated from the server, preserve selected and preloaded options in the option set.
<AutocompleteSelect
label="Course"
options={courseOptions}
selected={selectedCourse}
onSelect={setSelectedCourse}
onChangeSearchText={setSearch}
isLoading={isLoadingCourses}
placeholder="Search courses"
/>Add New Option
<AutocompleteSelect
label="Tag"
options={tagOptions}
selected={selectedTag}
onSelect={setSelectedTag}
onAddNewOption={(query) => createTag(query)}
/>When the current query does not exactly match an existing option label, an add-new row can appear at the bottom.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
selected | T | null | — | Current selected value. |
onSelect | (value: T | null) => void | Promise<void> | — | Called when a value is selected. May receive null. |
options | AutocompleteSelectOption[] | Record<string, AutocompleteSelectOption[]> | — | Required. Flat or grouped option data. |
label | string | — | Field label. |
id | string | — | Input id. |
onBlur | () => void | — | Blur handler for form integration. |
className | string | — | Wrapper class name. |
disabled | boolean | false | Disables interaction. |
required | boolean | false | Adds required asterisk in the label. |
size | 'md' | 'sm' | 'md' | Trigger/input size. |
Icon | ComponentType | — | Optional leading trigger icon. |
rightAdornment | IconName | 'ChevronBottom' | Right-side trigger icon. |
placement | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'bottom-start' | Dropdown placement. |
offset | number | 6 | Trigger/content spacing. |
placeholder | string | 'Select' | Placeholder text. |
error | string | null | — | Field error text. |
onAddNewOption | (query: string) => void | — | Called when the add-new row is chosen. |
onListEndReached | () => void | — | Called when the bottom sentinel becomes visible. |
onChangeSearchText | (value: string) => void | — | Called whenever search text changes. |
emptyStateTitle | string | — | Empty state title. |
emptyStateDescription | string | — | Empty state description. |
emptyStateAction | { label, onClick } | — | Optional empty state action button. |
buttonClassName | string | — | Additional trigger/input class name. |
labelIcon | IconName | — | Small icon next to the label. |
styleVariant | 'outline' | 'filled' | 'outline' | Trigger/input styling. |
isLoading | boolean | false | Shows loading spinner in the content panel. |
Option Shape
| Option prop | Type | Description |
|---|---|---|
value | T | Required picker value. |
label | string | Required display label used for search matching. |
disabled | boolean | Disables selection. |
marked | boolean | Adds a visual marked dot indicator. |
icon | IconName | Left icon for the option row. |
avatar | avatar subset | Avatar rendering for person/entity choices. |
render | () => ReactNode | Custom option rendering. |
How AutocompleteSelect Uses Picker
Internally, AutocompleteSelect is a more advanced picker composition:
- closed state:
Picker.Triggerwraps a custom button - open state:
Picker.Inputbecomes the editable trigger - content:
Picker.Content - grouped headings:
Picker.Content.Heading - options:
Picker.Item - empty state:
Picker.Empty
It also sets focusStrategy="none" on Picker because focus is managed by the autocomplete input itself.
Select Vs AutocompleteSelect
| Use case | Prefer |
|---|---|
| small/medium standard dropdown list | Select |
| optional in-menu search over a local list | Select searchable |
| type-to-filter is the primary interaction | AutocompleteSelect |
| grouped options | AutocompleteSelect |
| async data by itself | neither by default; keep selected options present first |
| add-new from current query | AutocompleteSelect |
Brand Dashboard Usage
Representative usages:
| Pattern | Path | Notes |
|---|---|---|
| Paginated field autocomplete | fsai/apps/brand-dashboard/src/modules/locations/components/CreateLocationModal/CreateLocationModal.tsx | Franchisee entity field with onListEndReached. |
| Local option autocomplete | fsai/apps/brand-dashboard/src/modules/locations/components/CreateLocationModal/CreateLocationModal.tsx | Deal zone field using the same field shell with local options. |
| Search-first assignment workflow | fsai/apps/brand-dashboard/src/components/AssigneeSearchDropdown/AssigneeSearchDropdown.tsx | Search-driven single selection outside a standard form. |
Important Cautions
- Object values must expose a stable
id. - Search text is cleared when the picker closes.
onChangeSearchTextshould expect that reset behavior.- Do not prefer
AutocompleteSelectjust because the backing data is async. - Keep selected and preloaded options available in
optionseven when search results are refreshed. - This is not a multi-select component; it is optimized for single search-driven selection.
Guidelines
- Do use
AutocompleteSelectwhen users need to search rather than scan - Do use grouped options when it improves discoverability
- Do use
onChangeSearchTextfor search-driven filtering while keeping selected options present - Do use
onAddNewOptionwhen creating from the typed query is part of the flow - Don't prefer
AutocompleteSelectjust because the data source is async - Don't use
AutocompleteSelectfor tiny fixed lists that would be simpler asSelect - Don't use raw
PickerwhenAutocompleteSelectalready matches the search-first pattern