Skip to main content

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).

note

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​

KeyAction
↓Open dropdown / Move down
↑Move up
EnterSelect highlighted item
TabSelect highlighted and move focus
EscapeClose 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.

PropDescription
nameForm field name. Forwarded to the inner <input>.
formOptional id of the form the input belongs to.
requiredMarks 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 label prop is automatically associated with the inner text input (htmlFor plus a generated id, or your own id if you pass one), so clicking the label focuses the input and assistive technology announces it. Inside a labeled Field, that Field's label names the input instead when it points at it: a Field that is not grouped or hasAddons points it there on its own when the Autocomplete has no id and no inner Field sits between them, and a label you wire by hand with labelProps.htmlFor points at the input with that id.
  • A user-supplied id is applied to the inner input (the labellable control), not the wrapper div.
  • Uses role="combobox" with aria-expanded, which is true only while the list is on screen. When nothing matches, the dropdown shows the empty content but no list, so it stays false
  • Has aria-haspopup="listbox" and aria-autocomplete="list", and, while the list is on screen, aria-controls pointing at it
  • Focus stays in the input, which points aria-activedescendant at the highlighted option as the arrow keys move it
  • The list is a role="listbox" named through aria-labelledby by the label that names the input, when that label has an id: the label prop's label always has one, and so does a Field's label that the Field wires itself. A Field label you wire by hand with labelProps.htmlFor, which is how a horizontal form names an Autocomplete in an inner Field, needs an id in its labelProps as well. Otherwise the list falls back to aria-label="Suggestions"
  • The list holds only the options. The header, footer and empty content 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 empty text is announced through a polite role="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 loading is on, or a Control with isLoading around the Autocomplete, the announcement waits, so the empty text 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. Pass loading while you fetch results, as the async story in Storybook does
  • Focusing the field, opening the list with openOnFocus or ↓, or emptying the field announces nothing. Without empty, nothing is announced at all, since there's no built-in message to say
  • empty is a message, so keep it to text. Tab closes 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" with aria-selected
  • Disabled items have aria-disabled
  • The clearable clear button is a native button named "Clear": Tab from the input reaches it, and Enter or Space clears the input and puts focus back in it
  • Full keyboard navigation support

  • Taginput - For multiple tag selection with autocomplete
  • Select - For simple dropdown selection
  • Input - For basic text input

Additional Resources​

Pro Tip

Use keepFirst combined with Enter to quickly select the first matching result without using the mouse.


Props​

PropTypeDefaultDescription
labelReact.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".
labelPropsReact.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.
idstring—Applied to the inner text input (the labellable control), not the wrapper div.
dataAutocompleteItem[] | string[][]The options data to display (required).
valuestring—The current input value (controlled).
selectedAutocompleteItem | string | null—The selected item (controlled).
placeholderstring—Placeholder text for the input.
fieldstring'label'Object property to use as the display field.
clearablebooleanfalseWhether 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.
openOnFocusbooleanfalseOpen dropdown when input is focused.
keepFirstbooleanfalseKeep first option highlighted.
keepOpenbooleanfalseKeep dropdown open after selection.
selectOnClickOutsidebooleanfalseSelect highlighted item on click outside.
maxHeightnumber200Maximum dropdown height in pixels.
dropdownbooleanfalseRender as dropdown style.
loadingbooleanfalseShow a loading spinner in the input. Under prefers-reduced-motion: reduce the spinner stops and stays drawn (with bestax's CSS loaded).
disabledbooleanfalseWhether the input is disabled.
checkInfiniteScrollbooleanfalseEnables infinite scroll detection in the dropdown.
infiniteScrollDistancenumber50Distance in pixels from the bottom to trigger onInfiniteScroll.
color'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'—Input color variant.
size'small' | 'medium' | 'large'—Size variant.
namestring—Form field name. Forwarded to the inner <input>.
formstring—Optional id of the form the input belongs to.
requiredbooleanfalseMarks 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.
headerReact.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.
footerReact.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.
emptyReact.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).
horizontalbooleanfalseHorizontal field layout.
messageReact.ReactNode—Help/validation message below the input.
messageColor'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'—Bulma color for the message.
fieldClassNamestring—Additional CSS classes for the Field wrapper.
classNamestring—Additional CSS classes.
refReact.Ref<HTMLInputElement>—Ref forwarded to the input element.
...All standard <div> attributes and Bulma helper props—See Helper Props

AutocompleteItem​

PropTypeDescription
valuestringThe item value.
labelstringDisplay label (optional).
disabledbooleanWhether 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 VariableSass VariableDefault
--bulma-autocomplete-dropdown-z-index$autocomplete-dropdown-z-index20
--bulma-autocomplete-dropdown-offset$autocomplete-dropdown-offset4px
--bulma-autocomplete-dropdown-background$autocomplete-dropdown-backgroundvar(--bulma-scheme-main)
--bulma-autocomplete-dropdown-radius$autocomplete-dropdown-radiusvar(--bulma-radius)
--bulma-autocomplete-dropdown-shadow$autocomplete-dropdown-shadowvar(--bulma-shadow)
--bulma-autocomplete-dropdown-padding-y$autocomplete-dropdown-padding-y0.5rem
--bulma-autocomplete-item-padding$autocomplete-item-padding0.375rem 1rem
--bulma-autocomplete-item-color$autocomplete-item-colorvar(--bulma-text)
--bulma-autocomplete-item-hover-background$autocomplete-item-hover-backgroundhsla(0, 0%, 50%, 0.13)
--bulma-autocomplete-item-hover-color$autocomplete-item-hover-colorvar(--bulma-text-strong)
--bulma-autocomplete-item-disabled-color$autocomplete-item-disabled-colorvar(--bulma-text-weak)
--bulma-autocomplete-item-disabled-opacity$autocomplete-item-disabled-opacity0.5
--bulma-autocomplete-header-footer-padding$autocomplete-header-footer-padding0.5rem 1rem
--bulma-autocomplete-header-footer-color$autocomplete-header-footer-colorvar(--bulma-text-weak)
--bulma-autocomplete-header-footer-size$autocomplete-header-footer-sizevar(--bulma-size-small)
--bulma-autocomplete-border-color$autocomplete-border-colorvar(--bulma-border)
--bulma-autocomplete-border-hover-color$autocomplete-border-hover-colorvar(--bulma-border-hover)
--bulma-autocomplete-icon-hover-color$autocomplete-icon-hover-colorvar(--bulma-text-strong)
--bulma-autocomplete-loader-border-color$autocomplete-loader-border-colorvar(--bulma-border)
--bulma-autocomplete-loader-active-color$autocomplete-loader-active-colorvar(--bulma-primary)
--bulma-autocomplete-loader-duration$autocomplete-loader-duration0.6s
--bulma-autocomplete-empty-color$autocomplete-empty-colorvar(--bulma-text-weak)
--bulma-autocomplete-scrollbar-width$autocomplete-scrollbar-width8px
--bulma-autocomplete-scrollbar-track$autocomplete-scrollbar-trackvar(--bulma-scheme-main-bis)
--bulma-autocomplete-scrollbar-thumb$autocomplete-scrollbar-thumbvar(--bulma-border)
--bulma-autocomplete-scrollbar-thumb-hover$autocomplete-scrollbar-thumb-hovervar(--bulma-border-hover)
--bulma-autocomplete-item-small-padding$autocomplete-item-small-padding0.25rem 0.75rem
--bulma-autocomplete-item-medium-padding$autocomplete-item-medium-padding0.5rem 1.25rem
--bulma-autocomplete-item-large-padding$autocomplete-item-large-padding0.625rem 1.5rem