useComboBox

Provides the behavior and accessibility implementation for a combo box component. A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query.

installyarn add @react-aria/combobox
version3.1.0
usageimport {useComboBox} from '@react-aria/combobox'

API#


useComboBox<T>( (props: AriaComboBoxProps<T>, , state: ComboBoxState<T> )): ComboBoxAria<T>

Features#


A combo box can be built using the <datalist> HTML element, but this is very limited in functionality and difficult to style. useComboBox helps achieve accessible combo box 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
  • 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 combo box 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 combo box 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#


Option oneOption twoOption threeOpLabelLabelInputButtonLabelList boxOp

A combo box consists of a label, an input which displays the current value, a list box popup, and an optional button used to toggle the list box popup open state. Users can type within the input to filter the available options 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 useComboBox, or by clicking or touching the button. useComboBox handles exposing the correct ARIA attributes for accessibility for each of the elements comprising the combo box. It should be combined with useListBox, which handles the implementation of the popup list box, and useButton which handles the button press interactions.

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

NameTypeDescription
labelPropsHTMLAttributes<HTMLElement>Props for the label element.
inputPropsInputHTMLAttributes<HTMLInputElement>Props for the combo box input element.
listBoxPropsAriaListBoxOptions<T>Props for the list box, to be passed to useListBox.
buttonPropsAriaButtonPropsProps for the optional trigger button, to be passed to useButton.
descriptionPropsHTMLAttributes<HTMLElement>Props for the combo box description element, if any.
errorMessagePropsHTMLAttributes<HTMLElement>Props for the combo box error message element, if any.

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

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

State management#


useComboBox requires knowledge of the options in the combo box 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 combo box is focused, and the current input value. For more information about selection, see Selection.

Example#


This example uses an <input> element for the combo box text input and a <button> element for the list box popup trigger. A <span> is included within the <button> to display the dropdown arrow icon (hidden from screen readers with aria-hidden). 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 uses useListBox and useOption to render the list of options. A hidden <DismissButton> is added at the end of the popup to allow screen reader users to dismiss the popup easily, without navigating back through the entire list. Note that shouldUseVirtualFocus is passed to useListBox and useOption so that browser focus remains within the <input> element even when interacting with the list box options.

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 {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useFilter} from '@react-aria/i18n';
import {useListBox, useOption} from '@react-aria/listbox';
import {useOverlay, DismissButton} from '@react-aria/overlays';

function ComboBox(props) {
  // Get a basic "contains" filter function for input value
  // and option text comparison
  let {contains} = useFilter({sensitivity: 'base'});

  // Create state based on the incoming props and the filter function
  let state = useComboBoxState({...props, defaultFilter: contains});

  let triggerRef = React.useRef();
  let inputRef = React.useRef();
  let listBoxRef = React.useRef();
  let popoverRef = React.useRef();

  // Get props for child elements from useComboBox
  let {
    buttonProps: triggerProps,
    inputProps,
    listBoxProps,
    labelProps
  } = useComboBox(
    {
      ...props,
      inputRef,
      buttonRef: triggerRef,
      listBoxRef,
      popoverRef,
      menuTrigger: 'input'
    },
    state
  );

  // Get props for the trigger button based on the
  // button props from useComboBox
  let {buttonProps} = useButton(triggerProps, triggerRef);

  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: 22,
            boxSizing: 'border-box',
            marginRight: 0
          }}
        />
        <button
          {...buttonProps}
          ref={triggerRef}
          style={{
            height: 22,
            marginLeft: 0
          }}>
          <span aria-hidden="true" style={{padding: '0 2px'}}></span>
        </button>
        {state.isOpen && (
          <ListBoxPopup
            {...listBoxProps}
            // Use virtual focus to get aria-activedescendant tracking and
            // ensure focus doesn't leave the input field
            shouldUseVirtualFocus
            listBoxRef={listBoxRef}
            popoverRef={popoverRef}
            state={state}
          />
        )}
      </div>
    </div>
  );
}

function ListBoxPopup(props) {
  let {
    popoverRef,
    listBoxRef,
    state,
    shouldUseVirtualFocus,
    ...otherProps
  } = props;

  // Get props for the list box.
  // Prevent focus moving to list box via shouldUseVirtualFocus
  let {listBoxProps} = useListBox(
    {
      autoFocus: state.focusStrategy,
      disallowEmptySelection: true,
      shouldUseVirtualFocus,
      ...otherProps
    },
    state,
    listBoxRef
  );

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

  // Add a hidden <DismissButton> component at the end of the list
  // to allow screen reader users to dismiss the popup easily.
  return (
    <div {...overlayProps} ref={popoverRef}>
      <ul
        {...mergeProps(listBoxProps, otherProps)}
        ref={listBoxRef}
        style={{
          position: 'absolute',
          width: '100%',
          margin: '4px 0 0 0',
          padding: 0,
          listStyle: 'none',
          border: '1px solid gray',
          background: 'lightgray'
        }}>
        {[...state.collection].map((item) => (
          <Option
            shouldUseVirtualFocus
            key={item.key}
            item={item}
            state={state}
          />
        ))}
      </ul>
      <DismissButton onDismiss={() => state.close()} />
    </div>
  );
}

function Option({item, state, shouldUseVirtualFocus}) {
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);
  let isSelected = state.selectionManager.isSelected(item.key);
  // Track focus via focusedKey state instead of with focus event listeners
  // since focus never leaves the text input in a ComboBox
  let isFocused = state.selectionManager.focusedKey === item.key;

  // Get props for the option element.
  // Prevent options from receiving browser focus via shouldUseVirtualFocus.
  let {optionProps} = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected,
      shouldSelectOnPressUp: true,
      shouldFocusOnHover: true,
      shouldUseVirtualFocus
    },
    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>
  );
}

<ComboBox label="Favorite Animal">
  <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>
</ComboBox>
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useFilter} from '@react-aria/i18n';
import {useListBox, useOption} from '@react-aria/listbox';
import {
  useOverlay,
  DismissButton
} from '@react-aria/overlays';

function ComboBox(props) {
  // Get a basic "contains" filter function for input value
  // and option text comparison
  let {contains} = useFilter({sensitivity: 'base'});

  // Create state based on the incoming props and the filter function
  let state = useComboBoxState({
    ...props,
    defaultFilter: contains
  });

  let triggerRef = React.useRef();
  let inputRef = React.useRef();
  let listBoxRef = React.useRef();
  let popoverRef = React.useRef();

  // Get props for child elements from useComboBox
  let {
    buttonProps: triggerProps,
    inputProps,
    listBoxProps,
    labelProps
  } = useComboBox(
    {
      ...props,
      inputRef,
      buttonRef: triggerRef,
      listBoxRef,
      popoverRef,
      menuTrigger: 'input'
    },
    state
  );

  // Get props for the trigger button based on the
  // button props from useComboBox
  let {buttonProps} = useButton(triggerProps, triggerRef);

  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: 22,
            boxSizing: 'border-box',
            marginRight: 0
          }}
        />
        <button
          {...buttonProps}
          ref={triggerRef}
          style={{
            height: 22,
            marginLeft: 0
          }}>
          <span
            aria-hidden="true"
            style={{padding: '0 2px'}}></span>
        </button>
        {state.isOpen && (
          <ListBoxPopup
            {...listBoxProps}
            // Use virtual focus to get aria-activedescendant tracking and
            // ensure focus doesn't leave the input field
            shouldUseVirtualFocus
            listBoxRef={listBoxRef}
            popoverRef={popoverRef}
            state={state}
          />
        )}
      </div>
    </div>
  );
}

function ListBoxPopup(props) {
  let {
    popoverRef,
    listBoxRef,
    state,
    shouldUseVirtualFocus,
    ...otherProps
  } = props;

  // Get props for the list box.
  // Prevent focus moving to list box via shouldUseVirtualFocus
  let {listBoxProps} = useListBox(
    {
      autoFocus: state.focusStrategy,
      disallowEmptySelection: true,
      shouldUseVirtualFocus,
      ...otherProps
    },
    state,
    listBoxRef
  );

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

  // Add a hidden <DismissButton> component at the end of the list
  // to allow screen reader users to dismiss the popup easily.
  return (
    <div {...overlayProps} ref={popoverRef}>
      <ul
        {...mergeProps(listBoxProps, otherProps)}
        ref={listBoxRef}
        style={{
          position: 'absolute',
          width: '100%',
          margin: '4px 0 0 0',
          padding: 0,
          listStyle: 'none',
          border: '1px solid gray',
          background: 'lightgray'
        }}>
        {[...state.collection].map((item) => (
          <Option
            shouldUseVirtualFocus
            key={item.key}
            item={item}
            state={state}
          />
        ))}
      </ul>
      <DismissButton onDismiss={() => state.close()} />
    </div>
  );
}

function Option({item, state, shouldUseVirtualFocus}) {
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);
  let isSelected = state.selectionManager.isSelected(
    item.key
  );
  // Track focus via focusedKey state instead of with focus event listeners
  // since focus never leaves the text input in a ComboBox
  let isFocused =
    state.selectionManager.focusedKey === item.key;

  // Get props for the option element.
  // Prevent options from receiving browser focus via shouldUseVirtualFocus.
  let {optionProps} = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected,
      shouldSelectOnPressUp: true,
      shouldFocusOnHover: true,
      shouldUseVirtualFocus
    },
    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>
  );
}

<ComboBox label="Favorite Animal">
  <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>
</ComboBox>
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useFilter} from '@react-aria/i18n';
import {
  useListBox,
  useOption
} from '@react-aria/listbox';
import {
  useOverlay,
  DismissButton
} from '@react-aria/overlays';

function ComboBox(
  props
) {
  // Get a basic "contains" filter function for input value
  // and option text comparison
  let {
    contains
  } = useFilter({
    sensitivity: 'base'
  });

  // Create state based on the incoming props and the filter function
  let state = useComboBoxState(
    {
      ...props,
      defaultFilter: contains
    }
  );

  let triggerRef = React.useRef();
  let inputRef = React.useRef();
  let listBoxRef = React.useRef();
  let popoverRef = React.useRef();

  // Get props for child elements from useComboBox
  let {
    buttonProps: triggerProps,
    inputProps,
    listBoxProps,
    labelProps
  } = useComboBox(
    {
      ...props,
      inputRef,
      buttonRef: triggerRef,
      listBoxRef,
      popoverRef,
      menuTrigger:
        'input'
    },
    state
  );

  // Get props for the trigger button based on the
  // button props from useComboBox
  let {
    buttonProps
  } = useButton(
    triggerProps,
    triggerRef
  );

  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: 22,
            boxSizing:
              'border-box',
            marginRight: 0
          }}
        />
        <button
          {...buttonProps}
          ref={
            triggerRef
          }
          style={{
            height: 22,
            marginLeft: 0
          }}>
          <span
            aria-hidden="true"
            style={{
              padding:
                '0 2px'
            }}></span>
        </button>
        {state.isOpen && (
          <ListBoxPopup
            {...listBoxProps}
            // Use virtual focus to get aria-activedescendant tracking and
            // ensure focus doesn't leave the input field
            shouldUseVirtualFocus
            listBoxRef={
              listBoxRef
            }
            popoverRef={
              popoverRef
            }
            state={state}
          />
        )}
      </div>
    </div>
  );
}

function ListBoxPopup(
  props
) {
  let {
    popoverRef,
    listBoxRef,
    state,
    shouldUseVirtualFocus,
    ...otherProps
  } = props;

  // Get props for the list box.
  // Prevent focus moving to list box via shouldUseVirtualFocus
  let {
    listBoxProps
  } = useListBox(
    {
      autoFocus:
        state.focusStrategy,
      disallowEmptySelection: true,
      shouldUseVirtualFocus,
      ...otherProps
    },
    state,
    listBoxRef
  );

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

  // Add a hidden <DismissButton> component at the end of the list
  // to allow screen reader users to dismiss the popup easily.
  return (
    <div
      {...overlayProps}
      ref={popoverRef}>
      <ul
        {...mergeProps(
          listBoxProps,
          otherProps
        )}
        ref={listBoxRef}
        style={{
          position:
            'absolute',
          width: '100%',
          margin:
            '4px 0 0 0',
          padding: 0,
          listStyle:
            'none',
          border:
            '1px solid gray',
          background:
            'lightgray'
        }}>
        {[
          ...state.collection
        ].map((item) => (
          <Option
            shouldUseVirtualFocus
            key={
              item.key
            }
            item={item}
            state={state}
          />
        ))}
      </ul>
      <DismissButton
        onDismiss={() =>
          state.close()
        }
      />
    </div>
  );
}

function Option({
  item,
  state,
  shouldUseVirtualFocus
}) {
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(
    item.key
  );
  let isSelected = state.selectionManager.isSelected(
    item.key
  );
  // Track focus via focusedKey state instead of with focus event listeners
  // since focus never leaves the text input in a ComboBox
  let isFocused =
    state
      .selectionManager
      .focusedKey ===
    item.key;

  // Get props for the option element.
  // Prevent options from receiving browser focus via shouldUseVirtualFocus.
  let {
    optionProps
  } = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected,
      shouldSelectOnPressUp: true,
      shouldFocusOnHover: true,
      shouldUseVirtualFocus
    },
    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>
  );
}

<ComboBox label="Favorite Animal">
  <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>
</ComboBox>

Usage#


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

Uncontrolled#

The following example shows how you would create an uncontrolled ComboBox. The input value, selected option, and open state is completely uncontrolled, with the default input text set by the defaultInputValue prop.

<ComboBox label="Favorite Animal" defaultInputValue="red">
  <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>
</ComboBox>
<ComboBox label="Favorite Animal" defaultInputValue="red">
  <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>
</ComboBox>
<ComboBox
  label="Favorite Animal"
  defaultInputValue="red">
  <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>
</ComboBox>

Dynamic collections#

ComboBox 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 ComboBox using the defaultItems prop. Each item accepts a key prop, which is passed to the onSelectionChange handler to identify the selected item. Alternatively, if the item objects contain an id property, as shown in the example below, then this is used automatically and a key prop is not required.

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 [majorId, setMajorId] = React.useState();

  return (
    <>
      <ComboBox
        label="Pick a engineering major"
        defaultItems={options}
        onSelectionChange={setMajorId}>
        {(item) => <Item>{item.name}</Item>}
      </ComboBox>
      <p>Selected topic id: {majorId}</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 [majorId, setMajorId] = React.useState();

  return (
    <>
      <ComboBox
        label="Pick a engineering major"
        defaultItems={options}
        onSelectionChange={setMajorId}>
        {(item) => <Item>{item.name}</Item>}
      </ComboBox>
      <p>Selected topic id: {majorId}</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 [
    majorId,
    setMajorId
  ] = React.useState();

  return (
    <>
      <ComboBox
        label="Pick a engineering major"
        defaultItems={
          options
        }
        onSelectionChange={
          setMajorId
        }>
        {(item) => (
          <Item>
            {item.name}
          </Item>
        )}
      </ComboBox>
      <p>
        Selected topic
        id: {majorId}
      </p>
    </>
  );
}

Allow custom values#

By default, useComboBoxState doesn't allow users to specify a value that doesn't exist in the list of options and will revert the input value to the current selected value on blur. By specifying allowsCustomValue, this behavior is suppressed and the user is free to enter any value within the field.

<ComboBox label="Favorite Animal" allowsCustomValue>
  <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>
</ComboBox>
<ComboBox label="Favorite Animal" allowsCustomValue>
  <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>
</ComboBox>
<ComboBox
  label="Favorite Animal"
  allowsCustomValue>
  <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>
</ComboBox>

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 ComboBox 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 (
    <ComboBox
      label="To:"
      items={filteredItems}
      inputValue={filterValue}
      onInputChange={setFilterValue}
      placeholder="Enter recipient email"
      allowsCustomValue>
      {(item) => <Item>{item.email}</Item>}
    </ComboBox>
  );
}
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 (
    <ComboBox
      label="To:"
      items={filteredItems}
      inputValue={filterValue}
      onInputChange={setFilterValue}
      placeholder="Enter recipient email"
      allowsCustomValue>
      {(item) => <Item>{item.email}</Item>}
    </ComboBox>
  );
}
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 (
    <ComboBox
      label="To:"
      items={
        filteredItems
      }
      inputValue={
        filterValue
      }
      onInputChange={
        setFilterValue
      }
      placeholder="Enter recipient email"
      allowsCustomValue>
      {(item) => (
        <Item>
          {item.email}
        </Item>
      )}
    </ComboBox>
  );
}

Fully controlled#

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

It is important to note that you don't have to control every single aspect of a ComboBox. If you decide to only control a single property of the ComboBox, be sure to provide the change handler for that prop as well e.g. controlling selectedKey would require onSelectionChange to be passed to useComboBox as well.

function ControlledComboBox() {
  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 ComboBox input value, selected option, open state, and items
  // in a state tracker
  let [fieldState, setFieldState] = React.useState({
    selectedKey: '',
    inputValue: '',
    items: optionList
  });

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

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

  // Specify how each of the ComboBox values should change when the input
  // field is altered by the user
  let onInputChange = (value) => {
    setFieldState((prevState) => ({
      inputValue: value,
      selectedKey: value === '' ? null : prevState.selectedKey,
      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,
        selectedKey: prevState.selectedKey,
        items: optionList
      }));
    }
  };

  // Pass each controlled prop to useComboBox along with their
  // change handlers
  return (
    <ComboBox
      label="Favorite Animal"
      items={fieldState.items}
      selectedKey={fieldState.selectedKey}
      inputValue={fieldState.inputValue}
      onOpenChange={onOpenChange}
      onSelectionChange={onSelectionChange}
      onInputChange={onInputChange}>
      {(item) => <Item>{item.name}</Item>}
    </ComboBox>
  );
}

<ControlledComboBox />
function ControlledComboBox() {
  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 ComboBox input value, selected option, open state, and items
  // in a state tracker
  let [fieldState, setFieldState] = React.useState({
    selectedKey: '',
    inputValue: '',
    items: optionList
  });

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

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

  // Specify how each of the ComboBox values should change when the input
  // field is altered by the user
  let onInputChange = (value) => {
    setFieldState((prevState) => ({
      inputValue: value,
      selectedKey:
        value === '' ? null : prevState.selectedKey,
      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,
        selectedKey: prevState.selectedKey,
        items: optionList
      }));
    }
  };

  // Pass each controlled prop to useComboBox along with their
  // change handlers
  return (
    <ComboBox
      label="Favorite Animal"
      items={fieldState.items}
      selectedKey={fieldState.selectedKey}
      inputValue={fieldState.inputValue}
      onOpenChange={onOpenChange}
      onSelectionChange={onSelectionChange}
      onInputChange={onInputChange}>
      {(item) => <Item>{item.name}</Item>}
    </ComboBox>
  );
}

<ControlledComboBox />
function ControlledComboBox() {
  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 ComboBox input value, selected option, open state, and items
  // in a state tracker
  let [
    fieldState,
    setFieldState
  ] = React.useState({
    selectedKey: '',
    inputValue: '',
    items: optionList
  });

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

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

  // Specify how each of the ComboBox values should change when the input
  // field is altered by the user
  let onInputChange = (
    value
  ) => {
    setFieldState(
      (prevState) => ({
        inputValue: value,
        selectedKey:
          value === ''
            ? null
            : prevState.selectedKey,
        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,
          selectedKey:
            prevState.selectedKey,
          items: optionList
        })
      );
    }
  };

  // Pass each controlled prop to useComboBox along with their
  // change handlers
  return (
    <ComboBox
      label="Favorite Animal"
      items={
        fieldState.items
      }
      selectedKey={
        fieldState.selectedKey
      }
      inputValue={
        fieldState.inputValue
      }
      onOpenChange={
        onOpenChange
      }
      onSelectionChange={
        onSelectionChange
      }
      onInputChange={
        onInputChange
      }>
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </ComboBox>
  );
}

<ControlledComboBox />

useComboBoxState supports three different menuTrigger prop values:

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

The example below has menuTrigger set to focus.

<ComboBox label="Favorite Animal" 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>
</ComboBox>
<ComboBox label="Favorite Animal" 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>
</ComboBox>
<ComboBox
  label="Favorite Animal"
  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>
</ComboBox>

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.

<ComboBox label="Favorite Animal" 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>
</ComboBox>
<ComboBox
  label="Favorite Animal"
  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>
</ComboBox>
<ComboBox
  label="Favorite Animal"
  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>
</ComboBox>

Internationalization#


useComboBox 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 combo box.

RTL#

In right-to-left languages, the ComboBox should be mirrored. The trigger button should be on the left, and the input element should be on the right. In addition, the content of ComboBox options should flip. Ensure that your CSS accounts for this.