Autocomplete
Overview
The Autocomplete component provides an input field with dropdown suggestions that filter based on user input.
It supports both string arrays and object data, keyboard navigation, and custom templates.
Import
import { Autocomplete } from '@allxsmith/bestax-bulma';
Usage
Basic Autocomplete
Simple autocomplete with string array. onSelect is typed for either kind of data, as string | AutocompleteItem | null, so narrow the item to the kind you passed before storing it.
function example() { const [selected, setSelected] = useState<string | null>(null); const fruits = [ 'Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape', ]; return ( <Block> <Autocomplete data={fruits} placeholder="Search fruit..." onSelect={item => setSelected(typeof item === 'string' ? item : null)} /> {selected && <Paragraph mt="2">Selected: {selected}</Paragraph>} </Block> ); }
Open on Focus
Dropdown opens immediately when input is focused.
function example() { const countries = [ 'United States', 'United Kingdom', 'Canada', 'Australia', 'Germany', 'France', ]; return ( <Autocomplete data={countries} placeholder="Select a country..." openOnFocus /> ); }
With Clear Button
Autocomplete with clearable input. The clear button shows while the input holds a value and hides while it is disabled. It also gives way to a loading spinner, which sits at the same right edge: the Autocomplete's own loading one, or the isLoading spinner of a Control around it.
function example() { const options = ['Option 1', 'Option 2', 'Option 3', 'Option 4']; return ( <Autocomplete data={options} placeholder="Search options..." clearable /> ); }
Color and Size Variants
Autocomplete with different colors and sizes.
<Block display="flex" flexDirection="column"> <Autocomplete data={['Apple', 'Banana', 'Cherry']} placeholder="Primary small" color="primary" size="small" /> <Autocomplete data={['Apple', 'Banana', 'Cherry']} placeholder="Info normal" color="info" /> <Autocomplete data={['Apple', 'Banana', 'Cherry']} placeholder="Success medium" color="success" size="medium" /> <Autocomplete data={['Apple', 'Banana', 'Cherry']} placeholder="Danger large" color="danger" size="large" /> </Block>
With Object Data
Autocomplete with object items. onSelect passes back the item you picked, so looking it up in your own array by its value gives it back with your fields typed.
function example() { const users = [ { value: '1', label: 'John Doe', email: 'john@example.com' }, { value: '2', label: 'Jane Smith', email: 'jane@example.com' }, { value: '3', label: 'Bob Johnson', email: 'bob@example.com' }, ]; const [selected, setSelected] = useState<(typeof users)[number] | null>(null); return ( <Block> <Autocomplete data={users} field="label" placeholder="Search users..." openOnFocus onSelect={item => setSelected( users.find( u => u.value === (typeof item === 'string' ? item : item?.value) ) ?? null ) } /> {selected && ( <Paragraph mt="2"> Selected: {selected.label} ({selected.email}) </Paragraph> )} </Block> ); }
Custom Item Template
Autocomplete with custom item rendering. itemTemplate receives each item the same way, so the template looks it up by value too.
function example() { const users = [ { value: '1', label: 'John Doe', role: 'Admin' }, { value: '2', label: 'Jane Smith', role: 'Editor' }, { value: '3', label: 'Bob Johnson', role: 'Viewer' }, ]; return ( <Autocomplete data={users} field="label" placeholder="Search users..." openOnFocus itemTemplate={item => { const user = users.find( u => u.value === (typeof item === 'string' ? item : item.value) ); return ( <Block display="flex" justifyContent="space-between"> <Span>{user?.label}</Span> <Tag color="info" isLight> {user?.role} </Tag> </Block> ); }} /> ); }
With Empty State
Custom content when no results match. Screen reader users hear it too, once their typing runs the matches out, so keep it to a static message (see Accessibility).
function example() { const options = ['Apple', 'Banana', 'Cherry']; return ( <Autocomplete data={options} placeholder="Try searching 'xyz'..." empty={ <Paragraph textColor="grey"> <Icon name="search" variant="solid" mr="2" aria-hidden="true" /> No results found </Paragraph> } /> ); }
Loading State
Autocomplete showing loading indicator.
<Autocomplete data={[]} placeholder="Loading..." loading />
Keep First Highlighted
First option stays highlighted while typing.
function example() { const fruits = [ 'Apple', 'Apricot', 'Avocado', 'Banana', 'Blackberry', 'Blueberry', ]; return ( <Autocomplete data={fruits} placeholder="Type to filter..." keepFirst /> ); }
Context-Aware Rendering
The Autocomplete component is context-aware: it detects whether it is already inside a Field and adjusts its rendering accordingly. This means you can use it standalone with a label prop (it wraps itself in a Field), or inside a Field (it skips rendering its own).
Autocomplete does not use ControlContext, so the "With Field and Control Wrappers" example below uses Field wrapping only. The Control wrapper is shown for layout consistency. The only thing it changes inside the Autocomplete is the clear button, which gives way to that Control's isLoading spinner (see With Clear Button).
Default (with label)
The simplest usage — the component automatically renders its own Field wrapper.
<Autocomplete label="Fruit" data={['Apple', 'Banana', 'Cherry']} placeholder="Search fruit..." />
With Field Wrapper
When you need manual control over the Field layout (e.g., horizontal forms), wrap the component in Field. The component detects it's inside a Field and skips rendering its own.
A labeled Field that holds the Autocomplete directly names its input and its open list with no extra wiring. In a horizontal form the label sits on the outer Field and the Autocomplete in an inner one, which starts its own scope, so these examples wire the label by hand. An htmlFor in labelProps and a matching id on the Autocomplete name the input, and an id in labelProps gives the open list the label to take its name from. Without that id the list keeps its fallback name, "Suggestions".
function example() { return ( <Field horizontal label="Fruit" labelProps={{ htmlFor: 'fruit-field', id: 'fruit-field-label' }} > <Field.Body> <Field> <Autocomplete id="fruit-field" data={['Apple', 'Banana', 'Cherry']} placeholder="Search fruit..." /> </Field> </Field.Body> </Field> ); }
With Field and Control Wrappers
For full manual composition with icons, wrap in both Field and Control. Autocomplete does not consume ControlContext, but the Field wrapper is still detected and its own Field is skipped.
function example() { return ( <Field horizontal label="Fruit" labelProps={{ htmlFor: 'fruit-field-control', id: 'fruit-field-control-label', }} > <Field.Body> <Field> <Control iconLeftName="search"> <Autocomplete id="fruit-field-control" data={['Apple', 'Banana', 'Cherry']} placeholder="Search fruit..." /> </Control> </Field> </Field.Body> </Field> ); }
Keyboard Navigation
| Key | Action |
|---|---|
↓ | Open dropdown / Move down |
↑ | Move up |
Enter | Select highlighted item |
Tab | Select highlighted and move focus |
Escape | Close dropdown |
Form Submission
Autocomplete is an HTML form element. Pass a name prop and the typed/selected text is forwarded to the inner <input> so the value submits with the surrounding form, exactly like a native text input.
| Prop | Description |
|---|---|
name | Form field name. Forwarded to the inner <input>. |
form | Optional id of the form the input belongs to. |
required | Marks the field as required for native HTML form validation. |
function AutocompleteFormDemo() { const [submitted, setSubmitted] = React.useState(''); return ( <form onSubmit={e => { e.preventDefault(); const fd = new FormData(e.currentTarget); setSubmitted(JSON.stringify(Array.from(fd.entries()), null, 2)); }} > <Autocomplete name="city" data={['New York', 'London', 'Paris', 'Tokyo', 'Sydney']} placeholder="Search a city…" /> <div style={{ marginTop: '1rem' }}> <button type="submit" className="button is-primary"> Submit </button> </div> {submitted && <pre style={{ marginTop: '1rem' }}>{submitted}</pre>} </form> ); }
Accessibility
- The
labelprop is automatically associated with the inner text input (htmlForplus a generatedid, or your ownidif you pass one), so clicking the label focuses the input and assistive technology announces it. Inside a labeledField, thatField's label names the input instead when it points at it: aFieldthat is notgroupedorhasAddonspoints it there on its own when the Autocomplete has noidand no innerFieldsits between them, and a label you wire by hand withlabelProps.htmlForpoints at the input with thatid. - A user-supplied
idis applied to the inner input (the labellable control), not the wrapper div. - Uses
role="combobox"witharia-expanded, which is true only while the list is on screen. When nothing matches, the dropdown shows theemptycontent but no list, so it stays false - Has
aria-haspopup="listbox"andaria-autocomplete="list", and, while the list is on screen,aria-controlspointing at it - Focus stays in the input, which points
aria-activedescendantat the highlighted option as the arrow keys move it - The list is a
role="listbox"named througharia-labelledbyby the label that names the input, when that label has an id: thelabelprop's label always has one, and so does aField's label that theFieldwires itself. AFieldlabel you wire by hand withlabelProps.htmlFor, which is how a horizontal form names an Autocomplete in an innerField, needs anidin itslabelPropsas well. Otherwise the list falls back toaria-label="Suggestions" - The list holds only the options. The
header,footerandemptycontent sit beside it in the dropdown, so a screen reader doesn't count them as options - When the user's typing leaves nothing that matches, the
emptytext is announced through a politerole="status"region. That region is in the page from the start, so it's there before the message arrives. It's said once each time the matches run out, not again on every keystroke while they stay out, and the next search that comes up empty is announced too: after matches come back, after the field is emptied, or after the list closes and the user types again - While
loadingis on, or aControlwithisLoadingaround the Autocomplete, the announcement waits, so theemptytext that shows while results are on their way isn't read out. It's said once loading ends with nothing matching, for each search that comes back empty. Passloadingwhile you fetch results, as the async story in Storybook does - Focusing the field, opening the list with
openOnFocusor↓, or emptying the field announces nothing. Withoutempty, nothing is announced at all, since there's no built-in message to say emptyis a message, so keep it to text.Tabcloses the list, so a keyboard user can't reach a link or a button in it- Keep that message static, like "No results found". Its text is read when the matches first run out and isn't read again while they stay out, so a message that names the query, like "No results for xyz", is only heard for the query that ran them out
- Dropdown items use
role="option"witharia-selected - Disabled items have
aria-disabled - The
clearableclear button is a native button named "Clear":Tabfrom the input reaches it, andEnterorSpaceclears the input and puts focus back in it - Full keyboard navigation support
Related Components
- Taginput - For multiple tag selection with autocomplete
- Select - For simple dropdown selection
- Input - For basic text input
Additional Resources
Use keepFirst combined with Enter to quickly select the first matching result without using the mouse.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | — | Field label, associated with the text input via htmlFor: through your id when provided, otherwise a generated one. Dropped inside an outer Field, whose label names the input instead when it points at it. The Field points its label there itself when the Autocomplete has no id and no inner Field sits between them (and the Field is not grouped or hasAddons); a label you wire by hand with labelProps.htmlFor points at the input with that id. The label naming the input also names the open suggestion list, through aria-labelledby, when it has an id: this label always does, and an outer Field's does unless you wired it by hand without an id in its labelProps. Otherwise the list is named "Suggestions". |
labelProps | React.LabelHTMLAttributes<HTMLLabelElement> & { [key: string]: unknown; } | — | Props for the label element. An explicit htmlFor here overrides the automatic association (the input then gets no generated id). While the label names the input, the suggestion list's aria-labelledby points at it, through an id here when you give one and a generated one otherwise. |
id | string | — | Applied to the inner text input (the labellable control), not the wrapper div. |
data | AutocompleteItem[] | string[] | [] | The options data to display (required). |
value | string | — | The current input value (controlled). |
selected | AutocompleteItem | string | null | — | The selected item (controlled). |
placeholder | string | — | Placeholder text for the input. |
field | string | 'label' | Object property to use as the display field. |
clearable | boolean | false | Whether to show a clear button while the input holds a value. It hides while the input is disabled, and gives way to a loading spinner at the same right edge: the one loading draws, or that of a Control with isLoading around the Autocomplete. |
openOnFocus | boolean | false | Open dropdown when input is focused. |
keepFirst | boolean | false | Keep first option highlighted. |
keepOpen | boolean | false | Keep dropdown open after selection. |
selectOnClickOutside | boolean | false | Select highlighted item on click outside. |
maxHeight | number | 200 | Maximum dropdown height in pixels. |
dropdown | boolean | false | Render as dropdown style. |
loading | boolean | false | Show a loading spinner in the input. Under prefers-reduced-motion: reduce the spinner stops and stays drawn (with bestax's CSS loaded). |
disabled | boolean | false | Whether the input is disabled. |
checkInfiniteScroll | boolean | false | Enables infinite scroll detection in the dropdown. |
infiniteScrollDistance | number | 50 | Distance in pixels from the bottom to trigger onInfiniteScroll. |
color | 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger' | — | Input color variant. |
size | 'small' | 'medium' | 'large' | — | Size variant. |
name | string | — | Form field name. Forwarded to the inner <input>. |
form | string | — | Optional id of the form the input belongs to. |
required | boolean | false | Marks the field as required for native HTML form validation. |
onInput | (value: string) => void | — | Callback when input value changes. |
onSelect | (item: AutocompleteItem | string | null) => void | — | Callback when item is selected. |
onActiveChange | (active: boolean) => void | — | Callback when dropdown active state changes. |
onInfiniteScroll | () => void | — | Callback when scrolled to bottom (infinite scroll). |
itemTemplate | (item: AutocompleteItem | string) => React.ReactNode | — | Custom render for items. |
header | React.ReactNode | — | Custom header at the top of the dropdown. It sits above the list of options rather than in it, so assistive technology doesn't count it as one. |
footer | React.ReactNode | — | Custom footer at the bottom of the dropdown. It sits below the list of options rather than in it, so assistive technology doesn't count it as one. |
empty | React.ReactNode | — | A message the dropdown shows in place of the options when nothing matches. Keep it to text: Tab closes the list, so a keyboard user can't reach a link or button in it. It sits beside the list of options rather than in it, so the input's aria-expanded stays false while it shows. When the user's typing leaves nothing that matches, a polite status region announces its text: once each time the matches run out. The text is read when they first run out, and isn't read again as it changes while they stay out, so a message that names the query is heard for that first query only. Keep it static. Opening the list without typing, or emptying the field, announces nothing. While loading, or a Control with isLoading around the Autocomplete, is on, it waits, and it's announced once loading ends with nothing matching. Without empty, the dropdown closes when nothing matches and nothing is announced. |
labelSize | 'small' | 'normal' | 'medium' | 'large' | — | Size for the label (used in horizontal layouts). |
horizontal | boolean | false | Horizontal field layout. |
message | React.ReactNode | — | Help/validation message below the input. |
messageColor | 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger' | — | Bulma color for the message. |
fieldClassName | string | — | Additional CSS classes for the Field wrapper. |
className | string | — | Additional CSS classes. |
ref | React.Ref<HTMLInputElement> | — | Ref forwarded to the input element. |
... | All standard <div> attributes and Bulma helper props | — | See Helper Props |
AutocompleteItem
| Prop | Type | Description |
|---|---|---|
value | string | The item value. |
label | string | Display label (optional). |
disabled | boolean | Whether item is disabled. |
CSS & Sass Variables
Autocomplete registers these variables on its own .autocomplete element. Override them there (or via className) — a value set on an ancestor is only inherited, and loses to the component-level declaration. See Theme.
| CSS Variable | Sass Variable | Default |
|---|---|---|
--bulma-autocomplete-dropdown-z-index | $autocomplete-dropdown-z-index | 20 |
--bulma-autocomplete-dropdown-offset | $autocomplete-dropdown-offset | 4px |
--bulma-autocomplete-dropdown-background | $autocomplete-dropdown-background | var(--bulma-scheme-main) |
--bulma-autocomplete-dropdown-radius | $autocomplete-dropdown-radius | var(--bulma-radius) |
--bulma-autocomplete-dropdown-shadow | $autocomplete-dropdown-shadow | var(--bulma-shadow) |
--bulma-autocomplete-dropdown-padding-y | $autocomplete-dropdown-padding-y | 0.5rem |
--bulma-autocomplete-item-padding | $autocomplete-item-padding | 0.375rem 1rem |
--bulma-autocomplete-item-color | $autocomplete-item-color | var(--bulma-text) |
--bulma-autocomplete-item-hover-background | $autocomplete-item-hover-background | hsla(0, 0%, 50%, 0.13) |
--bulma-autocomplete-item-hover-color | $autocomplete-item-hover-color | var(--bulma-text-strong) |
--bulma-autocomplete-item-disabled-color | $autocomplete-item-disabled-color | var(--bulma-text-weak) |
--bulma-autocomplete-item-disabled-opacity | $autocomplete-item-disabled-opacity | 0.5 |
--bulma-autocomplete-header-footer-padding | $autocomplete-header-footer-padding | 0.5rem 1rem |
--bulma-autocomplete-header-footer-color | $autocomplete-header-footer-color | var(--bulma-text-weak) |
--bulma-autocomplete-header-footer-size | $autocomplete-header-footer-size | var(--bulma-size-small) |
--bulma-autocomplete-border-color | $autocomplete-border-color | var(--bulma-border) |
--bulma-autocomplete-border-hover-color | $autocomplete-border-hover-color | var(--bulma-border-hover) |
--bulma-autocomplete-icon-hover-color | $autocomplete-icon-hover-color | var(--bulma-text-strong) |
--bulma-autocomplete-loader-border-color | $autocomplete-loader-border-color | var(--bulma-border) |
--bulma-autocomplete-loader-active-color | $autocomplete-loader-active-color | var(--bulma-primary) |
--bulma-autocomplete-loader-duration | $autocomplete-loader-duration | 0.6s |
--bulma-autocomplete-empty-color | $autocomplete-empty-color | var(--bulma-text-weak) |
--bulma-autocomplete-scrollbar-width | $autocomplete-scrollbar-width | 8px |
--bulma-autocomplete-scrollbar-track | $autocomplete-scrollbar-track | var(--bulma-scheme-main-bis) |
--bulma-autocomplete-scrollbar-thumb | $autocomplete-scrollbar-thumb | var(--bulma-border) |
--bulma-autocomplete-scrollbar-thumb-hover | $autocomplete-scrollbar-thumb-hover | var(--bulma-border-hover) |
--bulma-autocomplete-item-small-padding | $autocomplete-item-small-padding | 0.25rem 0.75rem |
--bulma-autocomplete-item-medium-padding | $autocomplete-item-medium-padding | 0.5rem 1.25rem |
--bulma-autocomplete-item-large-padding | $autocomplete-item-large-padding | 0.625rem 1.5rem |