useSelect

Provides the behavior and accessibility implementation for a select component. A select displays a collapsible list of options and allows a user to select one of them.

installyarn add @react-aria/select
version3.5.0
usageimport {useSelect} from '@react-aria/select'

API#


useSelect<T>( props: AriaSelectOptions<T>, state: SelectState<T>, ref: RefObject<HTMLElement> ): SelectAria

Features#


A select can be built using the <select> and <option> HTML elements, but this is not possible to style consistently cross browser, especially the options. useSelect helps achieve accessible select components that can be styled as needed without compromising on high quality interactions.

  • Exposed to assistive technology as a button with a listbox popup using ARIA (combined with useListBox)
  • Support for selecting a single option
  • Support for disabled options
  • Support for sections
  • Labeling support for accessibility
  • Support for mouse, touch, and keyboard interactions
  • Tab stop focus management
  • Keyboard support for opening the listbox using the arrow keys, including automatically focusing the first or last item accordingly
  • Typeahead to allow selecting options by typing text, even without opening the listbox
  • Browser autofill integration via a hidden native <select> element
  • Support for mobile form navigation via software keyboard
  • Mobile screen reader listbox dismissal support

Anatomy#


Select anatomy diagramValueTriggerLabelLabelMenuOption 1Option 1LabelLabelOption 1Option 2Option 3

A select consists of a label, a button which displays a selected value, and a listbox, displayed in a popup. Users can click, touch, or use the keyboard on the button to open the listbox popup. useSelect handles exposing the correct ARIA attributes for accessibility and handles the interactions for the select in its collapsed state. It should be combined with useListBox, which handles the implementation of the popup listbox.

useSelect returns props that you should spread onto the appropriate element:

NameTypeDescription
labelPropsHTMLAttributes<HTMLElement>Props for the label element.
triggerPropsAriaButtonPropsProps for the popup trigger element.
valuePropsHTMLAttributes<HTMLElement>Props for the element representing the selected value.
menuPropsAriaListBoxOptions<unknown>Props for the popup.
descriptionPropsHTMLAttributes<HTMLElement>Props for the select's description element, if any.
errorMessagePropsHTMLAttributes<HTMLElement>Props for the select's error message element, if any.

State is managed by the useSelectState hook from @react-stately/select. The state object should be passed as an option to useSelect

If a select 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#


useSelect requires knowledge of the options in the select 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 useSelectState from @react-stately/select implements a JSX based interface for building collections instead. See Collection Components for more information, and Collection Interface for internal details.

In addition, useSelectState manages the state necessary for multiple 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. For more information about selection, see Selection.

Example#


This example uses a <button> element for the trigger, with a <span> inside to hold the value, and another for the dropdown arrow icon (hidden from screen readers with aria-hidden). A <HiddenSelect> is used to render a hidden native <select>, which enables browser form autofill support.

The listbox popup uses useListBox and useOption to render the list of options. In addition, a <FocusScope> is used to automatically restore focus to the trigger when the popup closes. A hidden <DismissButton> is added at the start and end of the popup to allow screen reader users to dismiss the popup.

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 {useSelectState} from '@react-stately/select';
import {Item} from '@react-stately/collections';
import {HiddenSelect} from '@react-aria/select';
import {useListBox, useOption} from '@react-aria/listbox';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useFocus} from '@react-aria/interactions';
import {FocusScope} from '@react-aria/focus';
import {useOverlay, DismissButton} from '@react-aria/overlays';

function Select(props) {
  // Create state based on the incoming props
  let state = useSelectState(props);

  // Get props for child elements from useSelect
  let ref = React.useRef();
  let {labelProps, triggerProps, valueProps, menuProps} = useSelect(
    props,
    state,
    ref
  );

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

  return (
    <div style={{position: 'relative', display: 'inline-block'}}>
      <div {...labelProps}>{props.label}</div>
      <HiddenSelect
        state={state}
        triggerRef={ref}
        label={props.label}
        name={props.name}
      />
      <button {...buttonProps} ref={ref} style={{height: 30, fontSize: 14}}>
        <span {...valueProps}>
          {state.selectedItem
            ? state.selectedItem.rendered
            : 'Select an option'}
        </span>
        <span aria-hidden="true" style={{paddingLeft: 5}}></span>
      </button>
      {state.isOpen && <ListBoxPopup {...menuProps} state={state} />}
    </div>
  );
}

function ListBoxPopup({state, ...otherProps}) {
  let ref = React.useRef();

  // Get props for the listbox
  let {listBoxProps} = useListBox(
    {
      autoFocus: state.focusStrategy || true,
      disallowEmptySelection: true,
      ...otherProps
    },
    state,
    ref
  );

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

  // Wrap in <FocusScope> so that focus is restored back to the
  // trigger when the popup is closed. In addition, add hidden
  // <DismissButton> components at the start and end of the list
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope restoreFocus>
      <div {...overlayProps} ref={overlayRef}>
        <DismissButton onDismiss={() => state.close()} />
        <ul
          {...mergeProps(listBoxProps, otherProps)}
          ref={ref}
          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 key={item.key} item={item} state={state} />
          ))}
        </ul>
        <DismissButton onDismiss={() => state.close()} />
      </div>
    </FocusScope>
  );
}

function Option({item, state}) {
  // Get props for the option element
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);
  let isSelected = state.selectionManager.isSelected(item.key);
  let {optionProps} = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected,
      shouldSelectOnPressUp: true,
      shouldFocusOnHover: true
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused option
  let [isFocused, setFocused] = React.useState(false);
  let {focusProps} = useFocus({onFocusChange: setFocused});

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      style={{
        background: isSelected
          ? 'blueviolet'
          : isFocused
          ? 'gray'
          : 'transparent',
        color: isSelected || isFocused ? 'white' : 'black',
        padding: '2px 5px',
        outline: 'none',
        cursor: 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}

<Select label="Favorite Color">
  <Item>Red</Item>
  <Item>Orange</Item>
  <Item>Yellow</Item>
  <Item>Green</Item>
  <Item>Blue</Item>
  <Item>Purple</Item>
</Select>
import {useSelectState} from '@react-stately/select';
import {Item} from '@react-stately/collections';
import {HiddenSelect} from '@react-aria/select';
import {useListBox, useOption} from '@react-aria/listbox';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useFocus} from '@react-aria/interactions';
import {FocusScope} from '@react-aria/focus';
import {
  useOverlay,
  DismissButton
} from '@react-aria/overlays';

function Select(props) {
  // Create state based on the incoming props
  let state = useSelectState(props);

  // Get props for child elements from useSelect
  let ref = React.useRef();
  let {
    labelProps,
    triggerProps,
    valueProps,
    menuProps
  } = useSelect(props, state, ref);

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

  return (
    <div
      style={{
        position: 'relative',
        display: 'inline-block'
      }}>
      <div {...labelProps}>{props.label}</div>
      <HiddenSelect
        state={state}
        triggerRef={ref}
        label={props.label}
        name={props.name}
      />
      <button
        {...buttonProps}
        ref={ref}
        style={{height: 30, fontSize: 14}}>
        <span {...valueProps}>
          {state.selectedItem
            ? state.selectedItem.rendered
            : 'Select an option'}
        </span>
        <span aria-hidden="true" style={{paddingLeft: 5}}></span>
      </button>
      {state.isOpen && (
        <ListBoxPopup {...menuProps} state={state} />
      )}
    </div>
  );
}

function ListBoxPopup({state, ...otherProps}) {
  let ref = React.useRef();

  // Get props for the listbox
  let {listBoxProps} = useListBox(
    {
      autoFocus: state.focusStrategy || true,
      disallowEmptySelection: true,
      ...otherProps
    },
    state,
    ref
  );

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

  // Wrap in <FocusScope> so that focus is restored back to the
  // trigger when the popup is closed. In addition, add hidden
  // <DismissButton> components at the start and end of the list
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope restoreFocus>
      <div {...overlayProps} ref={overlayRef}>
        <DismissButton onDismiss={() => state.close()} />
        <ul
          {...mergeProps(listBoxProps, otherProps)}
          ref={ref}
          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
              key={item.key}
              item={item}
              state={state}
            />
          ))}
        </ul>
        <DismissButton onDismiss={() => state.close()} />
      </div>
    </FocusScope>
  );
}

function Option({item, state}) {
  // Get props for the option element
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);
  let isSelected = state.selectionManager.isSelected(
    item.key
  );
  let {optionProps} = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected,
      shouldSelectOnPressUp: true,
      shouldFocusOnHover: true
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused option
  let [isFocused, setFocused] = React.useState(false);
  let {focusProps} = useFocus({onFocusChange: setFocused});

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      style={{
        background: isSelected
          ? 'blueviolet'
          : isFocused
          ? 'gray'
          : 'transparent',
        color: isSelected || isFocused ? 'white' : 'black',
        padding: '2px 5px',
        outline: 'none',
        cursor: 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}

<Select label="Favorite Color">
  <Item>Red</Item>
  <Item>Orange</Item>
  <Item>Yellow</Item>
  <Item>Green</Item>
  <Item>Blue</Item>
  <Item>Purple</Item>
</Select>
import {useSelectState} from '@react-stately/select';
import {Item} from '@react-stately/collections';
import {HiddenSelect} from '@react-aria/select';
import {
  useListBox,
  useOption
} from '@react-aria/listbox';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useFocus} from '@react-aria/interactions';
import {FocusScope} from '@react-aria/focus';
import {
  useOverlay,
  DismissButton
} from '@react-aria/overlays';

function Select(props) {
  // Create state based on the incoming props
  let state = useSelectState(
    props
  );

  // Get props for child elements from useSelect
  let ref = React.useRef();
  let {
    labelProps,
    triggerProps,
    valueProps,
    menuProps
  } = useSelect(
    props,
    state,
    ref
  );

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

  return (
    <div
      style={{
        position:
          'relative',
        display:
          'inline-block'
      }}>
      <div
        {...labelProps}>
        {props.label}
      </div>
      <HiddenSelect
        state={state}
        triggerRef={ref}
        label={
          props.label
        }
        name={props.name}
      />
      <button
        {...buttonProps}
        ref={ref}
        style={{
          height: 30,
          fontSize: 14
        }}>
        <span
          {...valueProps}>
          {state.selectedItem
            ? state
                .selectedItem
                .rendered
            : 'Select an option'}
        </span>
        <span
          aria-hidden="true"
          style={{
            paddingLeft: 5
          }}></span>
      </button>
      {state.isOpen && (
        <ListBoxPopup
          {...menuProps}
          state={state}
        />
      )}
    </div>
  );
}

function ListBoxPopup({
  state,
  ...otherProps
}) {
  let ref = React.useRef();

  // Get props for the listbox
  let {
    listBoxProps
  } = useListBox(
    {
      autoFocus:
        state.focusStrategy ||
        true,
      disallowEmptySelection: true,
      ...otherProps
    },
    state,
    ref
  );

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

  // Wrap in <FocusScope> so that focus is restored back to the
  // trigger when the popup is closed. In addition, add hidden
  // <DismissButton> components at the start and end of the list
  // to allow screen reader users to dismiss the popup easily.
  return (
    <FocusScope
      restoreFocus>
      <div
        {...overlayProps}
        ref={overlayRef}>
        <DismissButton
          onDismiss={() =>
            state.close()
          }
        />
        <ul
          {...mergeProps(
            listBoxProps,
            otherProps
          )}
          ref={ref}
          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
                key={
                  item.key
                }
                item={
                  item
                }
                state={
                  state
                }
              />
            )
          )}
        </ul>
        <DismissButton
          onDismiss={() =>
            state.close()
          }
        />
      </div>
    </FocusScope>
  );
}

function Option({
  item,
  state
}) {
  // Get props for the option element
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(
    item.key
  );
  let isSelected = state.selectionManager.isSelected(
    item.key
  );
  let {
    optionProps
  } = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected,
      shouldSelectOnPressUp: true,
      shouldFocusOnHover: true
    },
    state,
    ref
  );

  // Handle focus events so we can apply highlighted
  // style to the focused option
  let [
    isFocused,
    setFocused
  ] = React.useState(
    false
  );
  let {
    focusProps
  } = useFocus({
    onFocusChange: setFocused
  });

  return (
    <li
      {...mergeProps(
        optionProps,
        focusProps
      )}
      ref={ref}
      style={{
        background: isSelected
          ? 'blueviolet'
          : isFocused
          ? 'gray'
          : 'transparent',
        color:
          isSelected ||
          isFocused
            ? 'white'
            : 'black',
        padding:
          '2px 5px',
        outline: 'none',
        cursor: 'pointer'
      }}>
      {item.rendered}
    </li>
  );
}

<Select label="Favorite Color">
  <Item>Red</Item>
  <Item>Orange</Item>
  <Item>Yellow</Item>
  <Item>Green</Item>
  <Item>Blue</Item>
  <Item>Purple</Item>
</Select>

Internationalization#


useSelect and useListBox handle some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching. You are responsible for localizing all labels and option content that is passed into the select.

RTL#

In right-to-left languages, the select should be mirrored. The arrow should be on the left, and the selected value should be on the right. In addition, the content of list options should flip. Ensure that your CSS accounts for this.