alpha

useSearchAutocomplete

Provides the behavior and accessibility implementation for a search autocomplete component. A search autocomplete combines a combobox with a searchfield, allowing users to filter a list of options to items matching a query.

installyarn add @react-aria/autocomplete
version3.0.0-alpha.1
usageimport {useSearchAutocomplete} from '@react-aria/autocomplete'

API#


useSearchAutocomplete<T>( (props: AriaSearchAutocompleteProps<T>, , state: ComboBoxState<T> )): SearchAutocompleteAria<T>

Features#


Autocomplete for search fields can be implemented using the <datalist> HTML element, but this has limited functionality and behaves differently across browsers. useSearchAutocomplete helps achieve accessible search field and autocomplete components that can be styled as needed.

  • Support for filtering a list of options by typing
  • Support for selecting a single option
  • Support for disabled options
  • Support for groups of items in sections
  • Support for custom user input values
  • Support for controlled and uncontrolled options, selection, input value, and open state
  • Support for custom filter functions
  • Async loading and infinite scrolling support
  • Support for virtualized scrolling for performance with long lists
  • Exposed to assistive technology as a combobox with ARIA
  • Labeling support for accessibility
  • Required and invalid states exposed to assistive technology via ARIA
  • Support for mouse, touch, and keyboard interactions
  • Keyboard support for opening the list box using the arrow keys, including automatically focusing the first or last item accordingly
  • Support for opening the list box when typing, on focus, or manually
  • Handles virtual clicks on the input from touch screen readers to toggle the list box
  • Virtual focus management for list box option navigation
  • Hides elements outside the input and list box from assistive technology while the list box is open in a portal
  • Custom localized announcements for option focusing, filtering, and selection using an ARIA live region to work around VoiceOver bugs

Anatomy#


Anatomy of useSearchAutocomplete

A search autocomplete consists of a label, an input which displays the current value, and a list box popup. Users can type within the input to see search suggestions within the list box. The list box popup may be opened by a variety of input field interactions specified by the menuTrigger prop provided to useSearchAutocomplete. useSearchAutocomplete handles exposing the correct ARIA attributes for accessibility for each of the elements comprising the search autocomplete. It should be combined with useListBox, which handles the implementation of the popup list box.

useSearchAutocomplete returns props that you should spread onto the appropriate elements:

NameTypeDescription
labelPropsHTMLAttributes<HTMLElement>Props for the label element.
inputPropsInputHTMLAttributes<HTMLInputElement>Props for the search input element.
listBoxPropsAriaListBoxOptions<T>Props for the list box, to be passed to useListBox.
clearButtonPropsAriaButtonPropsProps for the search input's clear button.

State is managed by the useComboBoxState hook from @react-stately/combobox. The state object should be passed as an option to useSearchAutocomplete.

If the search field does not have a visible label, an aria-label or aria-labelledby prop must be provided instead to identify it to assistive technology.

State management#


useSearchAutocomplete requires knowledge of the options in order to handle keyboard navigation and other interactions. It does this using the Collection interface, which is a generic interface to access sequential unique keyed data. You can implement this interface yourself, e.g. by using a prop to pass a list of item objects, but useComboBoxState from @react-stately/combobox implements a JSX based interface for building collections instead. See Collection Components for more information, and Collection Interface for internal details.

In addition, useComboBoxState manages the state necessary for single selection and exposes a SelectionManager, which makes use of the collection to provide an interface to update the selection state. It also holds state to track if the popup is open, if the search field is focused, and the current input value. For more information about selection, see Selection.

Example#


This example uses an <input> element for the search field. A "contains" filter function is obtained from useFilter and is passed to useComboBoxState so that the list box can be filtered based on the option text and the current input text.

The list box popup should use the same Popover and ListBox components created with useOverlay and useListBox that you may already have in your component library or application. These can be shared with other components such as a Select created with useSelect or a Dialog popover created with useDialog. The code for these components is also included below in the collapsed sections.

This example does not do any advanced popover positioning or portaling to escape its visual container. See useOverlayTrigger for an example of how to implement this using useOverlayPosition.

In addition, see useListBox for examples of sections (option groups), and more complex options.

import {Item} from '@react-stately/collections';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useSearchAutocomplete} from '@react-aria/autocomplete';
import {useFilter} from '@react-aria/i18n';

// Reuse the ListBox and Popover from your component library. See below for details.
import {ListBox, Popover} from 'your-component-library';

function SearchAutocomplete(props) {
  // Setup filter function and state.
  let {contains} = useFilter({sensitivity: 'base'});
  let state = useComboBoxState({...props, defaultFilter: contains});

  // Setup refs and get props for child elements.
  let inputRef = React.useRef(null);
  let listBoxRef = React.useRef(null);
  let popoverRef = React.useRef(null);

  let {
    inputProps,
    listBoxProps,
    labelProps,
    clearButtonProps
  } = useSearchAutocomplete(
    {
      ...props,
      popoverRef,
      listBoxRef,
      inputRef
    },
    state
  );

  let {buttonProps} = useButton(clearButtonProps);

  return (
    <div style={{display: 'inline-flex', flexDirection: 'column'}}>
      <label {...labelProps}>{props.label}</label>
      <div style={{position: 'relative', display: 'inline-block'}}>
        <input
          {...inputProps}
          ref={inputRef}
          style={{
            height: 24,
            boxSizing: 'border-box',
            marginRight: 0,
            fontSize: 16
          }}
        />
        {state.inputValue !== '' && <button {...buttonProps}></button>}
        {state.isOpen && (
          <Popover
            popoverRef={popoverRef}
            isOpen={state.isOpen}
            onClose={state.close}>
            <ListBox {...listBoxProps} listBoxRef={listBoxRef} state={state} />
          </Popover>
        )}
      </div>
    </div>
  );
}

<SearchAutocomplete label="Search Animals">
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
import {Item} from '@react-stately/collections';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useSearchAutocomplete} from '@react-aria/autocomplete';
import {useFilter} from '@react-aria/i18n';

// Reuse the ListBox and Popover from your component library. See below for details.
import {ListBox, Popover} from 'your-component-library';

function SearchAutocomplete(props) {
  // Setup filter function and state.
  let {contains} = useFilter({sensitivity: 'base'});
  let state = useComboBoxState({
    ...props,
    defaultFilter: contains
  });

  // Setup refs and get props for child elements.
  let inputRef = React.useRef(null);
  let listBoxRef = React.useRef(null);
  let popoverRef = React.useRef(null);

  let {
    inputProps,
    listBoxProps,
    labelProps,
    clearButtonProps
  } = useSearchAutocomplete(
    {
      ...props,
      popoverRef,
      listBoxRef,
      inputRef
    },
    state
  );

  let {buttonProps} = useButton(clearButtonProps);

  return (
    <div
      style={{
        display: 'inline-flex',
        flexDirection: 'column'
      }}>
      <label {...labelProps}>{props.label}</label>
      <div
        style={{
          position: 'relative',
          display: 'inline-block'
        }}>
        <input
          {...inputProps}
          ref={inputRef}
          style={{
            height: 24,
            boxSizing: 'border-box',
            marginRight: 0,
            fontSize: 16
          }}
        />
        {state.inputValue !== '' && (
          <button {...buttonProps}></button>
        )}
        {state.isOpen && (
          <Popover
            popoverRef={popoverRef}
            isOpen={state.isOpen}
            onClose={state.close}>
            <ListBox
              {...listBoxProps}
              listBoxRef={listBoxRef}
              state={state}
            />
          </Popover>
        )}
      </div>
    </div>
  );
}

<SearchAutocomplete label="Search Animals">
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
import {Item} from '@react-stately/collections';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useSearchAutocomplete} from '@react-aria/autocomplete';
import {useFilter} from '@react-aria/i18n';

// Reuse the ListBox and Popover from your component library. See below for details.
import {
  ListBox,
  Popover
} from 'your-component-library';

function SearchAutocomplete(
  props
) {
  // Setup filter function and state.
  let {
    contains
  } = useFilter({
    sensitivity: 'base'
  });
  let state = useComboBoxState(
    {
      ...props,
      defaultFilter: contains
    }
  );

  // Setup refs and get props for child elements.
  let inputRef = React.useRef(
    null
  );
  let listBoxRef = React.useRef(
    null
  );
  let popoverRef = React.useRef(
    null
  );

  let {
    inputProps,
    listBoxProps,
    labelProps,
    clearButtonProps
  } = useSearchAutocomplete(
    {
      ...props,
      popoverRef,
      listBoxRef,
      inputRef
    },
    state
  );

  let {
    buttonProps
  } = useButton(
    clearButtonProps
  );

  return (
    <div
      style={{
        display:
          'inline-flex',
        flexDirection:
          'column'
      }}>
      <label
        {...labelProps}>
        {props.label}
      </label>
      <div
        style={{
          position:
            'relative',
          display:
            'inline-block'
        }}>
        <input
          {...inputProps}
          ref={inputRef}
          style={{
            height: 24,
            boxSizing:
              'border-box',
            marginRight: 0,
            fontSize: 16
          }}
        />
        {state.inputValue !==
          '' && (
          <button
            {...buttonProps}></button>
        )}
        {state.isOpen && (
          <Popover
            popoverRef={
              popoverRef
            }
            isOpen={
              state.isOpen
            }
            onClose={
              state.close
            }>
            <ListBox
              {...listBoxProps}
              listBoxRef={
                listBoxRef
              }
              state={
                state
              }
            />
          </Popover>
        )}
      </div>
    </div>
  );
}

<SearchAutocomplete label="Search Animals">
  <Item key="red panda">
    Red Panda
  </Item>
  <Item key="cat">
    Cat
  </Item>
  <Item key="dog">
    Dog
  </Item>
  <Item key="aardvark">
    Aardvark
  </Item>
  <Item key="kangaroo">
    Kangaroo
  </Item>
  <Item key="snake">
    Snake
  </Item>
</SearchAutocomplete>

Popover#

The Popover component is used to contain the popup listbox for the SearchAutocomplete. It can be shared between many other components, including Select, Menu, Dialog, and others. See useOverlayTrigger for more examples of popovers.

Show code
import {useOverlay, DismissButton} from '@react-aria/overlays';
import {FocusScope} from '@react-aria/focus';

function Popover(props) {
  let ref = React.useRef();
  let {popoverRef = ref, isOpen, onClose, children} = props;

  // Handle events that should cause the popup to close,
  // e.g. blur, clicking outside, or pressing the escape key.
  let {overlayProps} = useOverlay(
    {
      isOpen,
      onClose,
      shouldCloseOnBlur: true,
      isDismissable: true
    },
    popoverRef
  );

  // Add a hidden <DismissButton> component at the end of the popover
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope restoreFocus>
      <div
        {...overlayProps}
        ref={popoverRef}
        style={{
          position: 'absolute',
          width: '100%',
          border: '1px solid gray',
          background: 'lightgray',
          marginTop: 4
        }}>
        {children}
        <DismissButton onDismiss={onClose} />
      </div>
    </FocusScope>
  );
}
import {
  useOverlay,
  DismissButton
} from '@react-aria/overlays';
import {FocusScope} from '@react-aria/focus';

function Popover(props) {
  let ref = React.useRef();
  let {popoverRef = ref, isOpen, onClose, children} = props;

  // Handle events that should cause the popup to close,
  // e.g. blur, clicking outside, or pressing the escape key.
  let {overlayProps} = useOverlay(
    {
      isOpen,
      onClose,
      shouldCloseOnBlur: true,
      isDismissable: true
    },
    popoverRef
  );

  // Add a hidden <DismissButton> component at the end of the popover
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope restoreFocus>
      <div
        {...overlayProps}
        ref={popoverRef}
        style={{
          position: 'absolute',
          width: '100%',
          border: '1px solid gray',
          background: 'lightgray',
          marginTop: 4
        }}>
        {children}
        <DismissButton onDismiss={onClose} />
      </div>
    </FocusScope>
  );
}
import {
  useOverlay,
  DismissButton
} from '@react-aria/overlays';
import {FocusScope} from '@react-aria/focus';

function Popover(props) {
  let ref = React.useRef();
  let {
    popoverRef = ref,
    isOpen,
    onClose,
    children
  } = props;

  // Handle events that should cause the popup to close,
  // e.g. blur, clicking outside, or pressing the escape key.
  let {
    overlayProps
  } = useOverlay(
    {
      isOpen,
      onClose,
      shouldCloseOnBlur: true,
      isDismissable: true
    },
    popoverRef
  );

  // Add a hidden <DismissButton> component at the end of the popover
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope
      restoreFocus>
      <div
        {...overlayProps}
        ref={popoverRef}
        style={{
          position:
            'absolute',
          width: '100%',
          border:
            '1px solid gray',
          background:
            'lightgray',
          marginTop: 4
        }}>
        {children}
        <DismissButton
          onDismiss={
            onClose
          }
        />
      </div>
    </FocusScope>
  );
}

ListBox#

The ListBox and Option components are used to show the filtered list of options as the user types in the SearchAutocomplete. They can also be shared with other components like a Select. See useListBox for more examples, including sections and more complex items.

Show code
import {useListBox, useOption} from '@react-aria/listbox';

function ListBox(props) {
  let ref = React.useRef();
  let {listBoxRef = ref, state} = props;
  let {listBoxProps} = useListBox(props, state, listBoxRef);

  return (
    <ul
      {...listBoxProps}
      ref={listBoxRef}
      style={{
        margin: 0,
        padding: 0,
        listStyle: 'none',
        maxHeight: '150px',
        overflow: 'auto'
      }}>
      {[...state.collection].map((item) => (
        <Option key={item.key} item={item} state={state} />
      ))}
    </ul>
  );
}

function Option({item, state}) {
  let ref = React.useRef();
  let {optionProps, isSelected, isFocused, isDisabled} = useOption(
    {key: item.key},
    state,
    ref
  );

  let backgroundColor;
  let color = 'black';

  if (isSelected) {
    backgroundColor = 'blueviolet';
    color = 'white';
  } else if (isFocused) {
    backgroundColor = 'gray';
  } else if (isDisabled) {
    backgroundColor = 'transparent';
    color = 'gray';
  }

  return (
    <li
      {...optionProps}
      ref={ref}
      style={{
        background: backgroundColor,
        color: color,
        padding: '2px 5px',
        outline: 'none',
        cursor: 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}
import {useListBox, useOption} from '@react-aria/listbox';

function ListBox(props) {
  let ref = React.useRef();
  let {listBoxRef = ref, state} = props;
  let {listBoxProps} = useListBox(props, state, listBoxRef);

  return (
    <ul
      {...listBoxProps}
      ref={listBoxRef}
      style={{
        margin: 0,
        padding: 0,
        listStyle: 'none',
        maxHeight: '150px',
        overflow: 'auto'
      }}>
      {[...state.collection].map((item) => (
        <Option key={item.key} item={item} state={state} />
      ))}
    </ul>
  );
}

function Option({item, state}) {
  let ref = React.useRef();
  let {
    optionProps,
    isSelected,
    isFocused,
    isDisabled
  } = useOption({key: item.key}, state, ref);

  let backgroundColor;
  let color = 'black';

  if (isSelected) {
    backgroundColor = 'blueviolet';
    color = 'white';
  } else if (isFocused) {
    backgroundColor = 'gray';
  } else if (isDisabled) {
    backgroundColor = 'transparent';
    color = 'gray';
  }

  return (
    <li
      {...optionProps}
      ref={ref}
      style={{
        background: backgroundColor,
        color: color,
        padding: '2px 5px',
        outline: 'none',
        cursor: 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}
import {
  useListBox,
  useOption
} from '@react-aria/listbox';

function ListBox(props) {
  let ref = React.useRef();
  let {
    listBoxRef = ref,
    state
  } = props;
  let {
    listBoxProps
  } = useListBox(
    props,
    state,
    listBoxRef
  );

  return (
    <ul
      {...listBoxProps}
      ref={listBoxRef}
      style={{
        margin: 0,
        padding: 0,
        listStyle:
          'none',
        maxHeight:
          '150px',
        overflow: 'auto'
      }}>
      {[
        ...state.collection
      ].map((item) => (
        <Option
          key={item.key}
          item={item}
          state={state}
        />
      ))}
    </ul>
  );
}

function Option({
  item,
  state
}) {
  let ref = React.useRef();
  let {
    optionProps,
    isSelected,
    isFocused,
    isDisabled
  } = useOption(
    {key: item.key},
    state,
    ref
  );

  let backgroundColor;
  let color = 'black';

  if (isSelected) {
    backgroundColor =
      'blueviolet';
    color = 'white';
  } else if (isFocused) {
    backgroundColor =
      'gray';
  } else if (
    isDisabled
  ) {
    backgroundColor =
      'transparent';
    color = 'gray';
  }

  return (
    <li
      {...optionProps}
      ref={ref}
      style={{
        background: backgroundColor,
        color: color,
        padding:
          '2px 5px',
        outline: 'none',
        cursor: 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}

Usage#


The following examples show how to use the SearchAutocomplete component created in the above example.

Uncontrolled#

The following example shows how you would create an uncontrolled SearchAutocomplete. The input value, selected option, and open state is completely uncontrolled.

<SearchAutocomplete label="Search Animals">
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete label="Search Animals">
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete label="Search Animals">
  <Item key="red panda">
    Red Panda
  </Item>
  <Item key="cat">
    Cat
  </Item>
  <Item key="dog">
    Dog
  </Item>
  <Item key="aardvark">
    Aardvark
  </Item>
  <Item key="kangaroo">
    Kangaroo
  </Item>
  <Item key="snake">
    Snake
  </Item>
</SearchAutocomplete>

Dynamic collections#

SearchAutocomplete follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time.

As seen below, an iterable list of options is passed to the SearchAutocomplete using the defaultItems prop. The input's value is passed to the onSubmit handler, along with a key if the event was triggered by selecting an item from the listbox.

function Example() {
  let options = [
    {id: 1, name: 'Aerospace'},
    {id: 2, name: 'Mechanical'},
    {id: 3, name: 'Civil'},
    {id: 4, name: 'Biomedical'},
    {id: 5, name: 'Nuclear'},
    {id: 6, name: 'Industrial'},
    {id: 7, name: 'Chemical'},
    {id: 8, name: 'Agricultural'},
    {id: 9, name: 'Electrical'}
  ];
  let [major, setMajor] = React.useState();

  let onSubmit = (value, key) => {
    if (value) {
      setMajor(value);
    } else if (key) {
      setMajor(options.find((o) => o.id === key).name);
    }
  };

  return (
    <>
      <SearchAutocomplete
        label="Search engineering majors"
        defaultItems={options}
        onSubmit={onSubmit}>
        {(item) => <Item>{item.name}</Item>}
      </SearchAutocomplete>
      <p>Results for: {major}</p>
    </>
  );
}
function Example() {
  let options = [
    {id: 1, name: 'Aerospace'},
    {id: 2, name: 'Mechanical'},
    {id: 3, name: 'Civil'},
    {id: 4, name: 'Biomedical'},
    {id: 5, name: 'Nuclear'},
    {id: 6, name: 'Industrial'},
    {id: 7, name: 'Chemical'},
    {id: 8, name: 'Agricultural'},
    {id: 9, name: 'Electrical'}
  ];
  let [major, setMajor] = React.useState();

  let onSubmit = (value, key) => {
    if (value) {
      setMajor(value);
    } else if (key) {
      setMajor(options.find((o) => o.id === key).name);
    }
  };

  return (
    <>
      <SearchAutocomplete
        label="Search engineering majors"
        defaultItems={options}
        onSubmit={onSubmit}>
        {(item) => <Item>{item.name}</Item>}
      </SearchAutocomplete>
      <p>Results for: {major}</p>
    </>
  );
}
function Example() {
  let options = [
    {
      id: 1,
      name: 'Aerospace'
    },
    {
      id: 2,
      name: 'Mechanical'
    },
    {
      id: 3,
      name: 'Civil'
    },
    {
      id: 4,
      name: 'Biomedical'
    },
    {
      id: 5,
      name: 'Nuclear'
    },
    {
      id: 6,
      name: 'Industrial'
    },
    {
      id: 7,
      name: 'Chemical'
    },
    {
      id: 8,
      name:
        'Agricultural'
    },
    {
      id: 9,
      name: 'Electrical'
    }
  ];
  let [
    major,
    setMajor
  ] = React.useState();

  let onSubmit = (
    value,
    key
  ) => {
    if (value) {
      setMajor(value);
    } else if (key) {
      setMajor(
        options.find(
          (o) =>
            o.id === key
        ).name
      );
    }
  };

  return (
    <>
      <SearchAutocomplete
        label="Search engineering majors"
        defaultItems={
          options
        }
        onSubmit={
          onSubmit
        }>
        {(item) => (
          <Item>
            {item.name}
          </Item>
        )}
      </SearchAutocomplete>
      <p>
        Results for:{' '}
        {major}
      </p>
    </>
  );
}

Custom filtering#

By default, useComboBoxState uses the filter function passed to the defaultFilter prop (in the above example, a "contains" function from useFilter). The filter function can be overridden by users of the SearchAutocomplete component by using the items prop to control the filtered list. When items is provided rather than defaultItems, useComboBoxState does no filtering of its own.

The following example makes the inputValue controlled, and updates the filtered list that is passed to the items prop when the input changes value.

function Example() {
  let options = [
    {id: 1, email: 'fake@email.com'},
    {id: 2, email: 'anotherfake@email.com'},
    {id: 3, email: 'bob@email.com'},
    {id: 4, email: 'joe@email.com'},
    {id: 5, email: 'yourEmail@email.com'},
    {id: 6, email: 'valid@email.com'},
    {id: 7, email: 'spam@email.com'},
    {id: 8, email: 'newsletter@email.com'},
    {id: 9, email: 'subscribe@email.com'}
  ];

  let {startsWith} = useFilter({sensitivity: 'base'});
  let [filterValue, setFilterValue] = React.useState('');
  let filteredItems = React.useMemo(
    () => options.filter((item) => startsWith(item.email, filterValue)),
    [options, filterValue]
  );

  return (
    <SearchAutocomplete
      label="To:"
      items={filteredItems}
      inputValue={filterValue}
      onInputChange={setFilterValue}
      placeholder="Enter recipient email">
      {(item) => <Item>{item.email}</Item>}
    </SearchAutocomplete>
  );
}
function Example() {
  let options = [
    {id: 1, email: 'fake@email.com'},
    {id: 2, email: 'anotherfake@email.com'},
    {id: 3, email: 'bob@email.com'},
    {id: 4, email: 'joe@email.com'},
    {id: 5, email: 'yourEmail@email.com'},
    {id: 6, email: 'valid@email.com'},
    {id: 7, email: 'spam@email.com'},
    {id: 8, email: 'newsletter@email.com'},
    {id: 9, email: 'subscribe@email.com'}
  ];

  let {startsWith} = useFilter({sensitivity: 'base'});
  let [filterValue, setFilterValue] = React.useState('');
  let filteredItems = React.useMemo(
    () =>
      options.filter((item) =>
        startsWith(item.email, filterValue)
      ),
    [options, filterValue]
  );

  return (
    <SearchAutocomplete
      label="To:"
      items={filteredItems}
      inputValue={filterValue}
      onInputChange={setFilterValue}
      placeholder="Enter recipient email">
      {(item) => <Item>{item.email}</Item>}
    </SearchAutocomplete>
  );
}
function Example() {
  let options = [
    {
      id: 1,
      email:
        'fake@email.com'
    },
    {
      id: 2,
      email:
        'anotherfake@email.com'
    },
    {
      id: 3,
      email:
        'bob@email.com'
    },
    {
      id: 4,
      email:
        'joe@email.com'
    },
    {
      id: 5,
      email:
        'yourEmail@email.com'
    },
    {
      id: 6,
      email:
        'valid@email.com'
    },
    {
      id: 7,
      email:
        'spam@email.com'
    },
    {
      id: 8,
      email:
        'newsletter@email.com'
    },
    {
      id: 9,
      email:
        'subscribe@email.com'
    }
  ];

  let {
    startsWith
  } = useFilter({
    sensitivity: 'base'
  });
  let [
    filterValue,
    setFilterValue
  ] = React.useState('');
  let filteredItems = React.useMemo(
    () =>
      options.filter(
        (item) =>
          startsWith(
            item.email,
            filterValue
          )
      ),
    [
      options,
      filterValue
    ]
  );

  return (
    <SearchAutocomplete
      label="To:"
      items={
        filteredItems
      }
      inputValue={
        filterValue
      }
      onInputChange={
        setFilterValue
      }
      placeholder="Enter recipient email">
      {(item) => (
        <Item>
          {item.email}
        </Item>
      )}
    </SearchAutocomplete>
  );
}

Fully controlled#

The following example shows how you would create a controlled SearchAutocomplete, by controlling the input value (inputValue) and the autocomplete options (items). By passing in inputValue and items to the SearchAutocomplete you can control exactly what your SearchAutocomplete should display. For example, note that the item filtering for the controlled SearchAutocomplete below now follows a "starts with" filter strategy, accomplished by controlling the exact set of items available to the SearchAutocomplete whenever the input value updates.

function ControlledSearchAutocomplete() {
  let optionList = [
    {name: 'Red Panda', id: '1'},
    {name: 'Cat', id: '2'},
    {name: 'Dog', id: '3'},
    {name: 'Aardvark', id: '4'},
    {name: 'Kangaroo', id: '5'},
    {name: 'Snake', id: '6'}
  ];

  // Store SearchAutocomplete input value, selected option, open state, and items
  // in a state tracker
  let [fieldState, setFieldState] = React.useState({
    inputValue: '',
    items: optionList
  });

  // Implement custom filtering logic and control what items are
  // available to the SearchAutocomplete.
  let {startsWith} = useFilter({sensitivity: 'base'});

  // Specify how each of the SearchAutocomplete values should change when an
  // option is selected from the list box
  let onSubmit = (value, key) => {
    setFieldState((prevState) => {
      let selectedItem = prevState.items.find((option) => option.id === key);
      return {
        inputValue: selectedItem?.name ?? '',
        items: optionList.filter((item) =>
          startsWith(item.name, selectedItem?.name ?? '')
        )
      };
    });
  };

  // Specify how each of the SearchAutocomplete values should change when the input
  // field is altered by the user
  let onInputChange = (value) => {
    setFieldState((prevState) => ({
      inputValue: value,
      items: optionList.filter((item) => startsWith(item.name, value))
    }));
  };

  // Show entire list if user opens the menu manually
  let onOpenChange = (isOpen, menuTrigger) => {
    if (menuTrigger === 'manual' && isOpen) {
      setFieldState((prevState) => ({
        inputValue: prevState.inputValue,
        items: optionList
      }));
    }
  };

  // Pass each controlled prop to useSearchAutocomplete along with their
  // change handlers
  return (
    <SearchAutocomplete
      label="Search Animals"
      items={fieldState.items}
      inputValue={fieldState.inputValue}
      onOpenChange={onOpenChange}
      onSubmit={onSubmit}
      onInputChange={onInputChange}>
      {(item) => <Item>{item.name}</Item>}
    </SearchAutocomplete>
  );
}

<ControlledSearchAutocomplete />
function ControlledSearchAutocomplete() {
  let optionList = [
    {name: 'Red Panda', id: '1'},
    {name: 'Cat', id: '2'},
    {name: 'Dog', id: '3'},
    {name: 'Aardvark', id: '4'},
    {name: 'Kangaroo', id: '5'},
    {name: 'Snake', id: '6'}
  ];

  // Store SearchAutocomplete input value, selected option, open state, and items
  // in a state tracker
  let [fieldState, setFieldState] = React.useState({
    inputValue: '',
    items: optionList
  });

  // Implement custom filtering logic and control what items are
  // available to the SearchAutocomplete.
  let {startsWith} = useFilter({sensitivity: 'base'});

  // Specify how each of the SearchAutocomplete values should change when an
  // option is selected from the list box
  let onSubmit = (value, key) => {
    setFieldState((prevState) => {
      let selectedItem = prevState.items.find(
        (option) => option.id === key
      );
      return {
        inputValue: selectedItem?.name ?? '',
        items: optionList.filter((item) =>
          startsWith(item.name, selectedItem?.name ?? '')
        )
      };
    });
  };

  // Specify how each of the SearchAutocomplete values should change when the input
  // field is altered by the user
  let onInputChange = (value) => {
    setFieldState((prevState) => ({
      inputValue: value,
      items: optionList.filter((item) =>
        startsWith(item.name, value)
      )
    }));
  };

  // Show entire list if user opens the menu manually
  let onOpenChange = (isOpen, menuTrigger) => {
    if (menuTrigger === 'manual' && isOpen) {
      setFieldState((prevState) => ({
        inputValue: prevState.inputValue,
        items: optionList
      }));
    }
  };

  // Pass each controlled prop to useSearchAutocomplete along with their
  // change handlers
  return (
    <SearchAutocomplete
      label="Search Animals"
      items={fieldState.items}
      inputValue={fieldState.inputValue}
      onOpenChange={onOpenChange}
      onSubmit={onSubmit}
      onInputChange={onInputChange}>
      {(item) => <Item>{item.name}</Item>}
    </SearchAutocomplete>
  );
}

<ControlledSearchAutocomplete />
function ControlledSearchAutocomplete() {
  let optionList = [
    {
      name: 'Red Panda',
      id: '1'
    },
    {
      name: 'Cat',
      id: '2'
    },
    {
      name: 'Dog',
      id: '3'
    },
    {
      name: 'Aardvark',
      id: '4'
    },
    {
      name: 'Kangaroo',
      id: '5'
    },
    {
      name: 'Snake',
      id: '6'
    }
  ];

  // Store SearchAutocomplete input value, selected option, open state, and items
  // in a state tracker
  let [
    fieldState,
    setFieldState
  ] = React.useState({
    inputValue: '',
    items: optionList
  });

  // Implement custom filtering logic and control what items are
  // available to the SearchAutocomplete.
  let {
    startsWith
  } = useFilter({
    sensitivity: 'base'
  });

  // Specify how each of the SearchAutocomplete values should change when an
  // option is selected from the list box
  let onSubmit = (
    value,
    key
  ) => {
    setFieldState(
      (prevState) => {
        let selectedItem = prevState.items.find(
          (option) =>
            option.id ===
            key
        );
        return {
          inputValue:
            selectedItem?.name ??
            '',
          items: optionList.filter(
            (item) =>
              startsWith(
                item.name,
                selectedItem?.name ??
                  ''
              )
          )
        };
      }
    );
  };

  // Specify how each of the SearchAutocomplete values should change when the input
  // field is altered by the user
  let onInputChange = (
    value
  ) => {
    setFieldState(
      (prevState) => ({
        inputValue: value,
        items: optionList.filter(
          (item) =>
            startsWith(
              item.name,
              value
            )
        )
      })
    );
  };

  // Show entire list if user opens the menu manually
  let onOpenChange = (
    isOpen,
    menuTrigger
  ) => {
    if (
      menuTrigger ===
        'manual' &&
      isOpen
    ) {
      setFieldState(
        (prevState) => ({
          inputValue:
            prevState.inputValue,
          items: optionList
        })
      );
    }
  };

  // Pass each controlled prop to useSearchAutocomplete along with their
  // change handlers
  return (
    <SearchAutocomplete
      label="Search Animals"
      items={
        fieldState.items
      }
      inputValue={
        fieldState.inputValue
      }
      onOpenChange={
        onOpenChange
      }
      onSubmit={onSubmit}
      onInputChange={
        onInputChange
      }>
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </SearchAutocomplete>
  );
}

<ControlledSearchAutocomplete />

useComboBoxState supports three different menuTrigger prop values:

  • input (default): SearchAutocomplete menu opens when the user edits the input text.
  • focus: SearchAutocomplete menu opens when the user focuses the SearchAutocomplete input.
  • manual: SearchAutocomplete menu only opens when the user presses the trigger button or uses the arrow keys.

The example below has menuTrigger set to focus.

<SearchAutocomplete label="Search Animals" menuTrigger="focus">
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
  label="Search Animals"
  menuTrigger="focus">
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
  label="Search Animals"
  menuTrigger="focus">
  <Item key="red panda">
    Red Panda
  </Item>
  <Item key="cat">
    Cat
  </Item>
  <Item key="dog">
    Dog
  </Item>
  <Item key="aardvark">
    Aardvark
  </Item>
  <Item key="kangaroo">
    Kangaroo
  </Item>
  <Item key="snake">
    Snake
  </Item>
</SearchAutocomplete>

Disabled options#

You can disable specific options by providing an array of keys to useComboBoxState via the disabledKeys prop. This will prevent options with matching keys from being pressable and receiving keyboard focus as shown in the example below. Note that you are responsible for the styling of disabled options.

<SearchAutocomplete label="Search Animals" disabledKeys={['cat', 'kangaroo']}>
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
  label="Search Animals"
  disabledKeys={['cat', 'kangaroo']}>
  <Item key="red panda">Red Panda</Item>
  <Item key="cat">Cat</Item>
  <Item key="dog">Dog</Item>
  <Item key="aardvark">Aardvark</Item>
  <Item key="kangaroo">Kangaroo</Item>
  <Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
  label="Search Animals"
  disabledKeys={[
    'cat',
    'kangaroo'
  ]}>
  <Item key="red panda">
    Red Panda
  </Item>
  <Item key="cat">
    Cat
  </Item>
  <Item key="dog">
    Dog
  </Item>
  <Item key="aardvark">
    Aardvark
  </Item>
  <Item key="kangaroo">
    Kangaroo
  </Item>
  <Item key="snake">
    Snake
  </Item>
</SearchAutocomplete>

Asynchronous loading#

This example uses the useAsyncList hook to handle asynchronous loading and filtering of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data.

import {useAsyncList} from '@react-stately/data';

function AsyncLoadingExample() {
  let list = useAsyncList({
    async load({signal, filterText}) {
      let res = await fetch(
        `https://swapi.dev/api/people/?search=${filterText}`,
        {signal}
      );
      let json = await res.json();

      return {
        items: json.results
      };
    }
  });

  return (
    <SearchAutocomplete
      label="Search Star Wars Characters"
      items={list.items}
      inputValue={list.filterText}
      onInputChange={list.setFilterText}>
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </SearchAutocomplete>
  );
}
import {useAsyncList} from '@react-stately/data';

function AsyncLoadingExample() {
  let list = useAsyncList({
    async load({signal, filterText}) {
      let res = await fetch(
        `https://swapi.dev/api/people/?search=${filterText}`,
        {signal}
      );
      let json = await res.json();

      return {
        items: json.results
      };
    }
  });

  return (
    <SearchAutocomplete
      label="Search Star Wars Characters"
      items={list.items}
      inputValue={list.filterText}
      onInputChange={list.setFilterText}>
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </SearchAutocomplete>
  );
}
import {useAsyncList} from '@react-stately/data';

function AsyncLoadingExample() {
  let list = useAsyncList(
    {
      async load({
        signal,
        filterText
      }) {
        let res = await fetch(
          `https://swapi.dev/api/people/?search=${filterText}`,
          {signal}
        );
        let json = await res.json();

        return {
          items:
            json.results
        };
      }
    }
  );

  return (
    <SearchAutocomplete
      label="Search Star Wars Characters"
      items={list.items}
      inputValue={
        list.filterText
      }
      onInputChange={
        list.setFilterText
      }>
      {(item) => (
        <Item
          key={
            item.name
          }>
          {item.name}
        </Item>
      )}
    </SearchAutocomplete>
  );
}

Internationalization#


useSearchAutocomplete handles some aspects of internationalization automatically. For example, the item focus, count, and selection VoiceOver announcements are localized. You are responsible for localizing all labels and option content that is passed into the autocomplete.