useListBox

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

installyarn add @react-aria/listbox
version3.3.1
usageimport {useListBox, useOption, useListBoxSection} from '@react-aria/listbox'

API#


useListBox<T>( props: AriaListBoxOptions<T>, state: ListState<T>, ref: RefObject<HTMLElement> ): ListBoxAriauseOption<T>( props: AriaOptionProps, state: ListState<T>, ref: RefObject<HTMLElement> ): OptionAriauseListBoxSection( (props: AriaListBoxSectionProps )): ListBoxSectionAria

Features#


A listbox can be built using the <select> and <option> HTML elements, but this is not possible to style consistently cross browser. useListBox helps achieve accessible listbox components that can be styled as needed.

Note: useListBox only handles the list itself. For a dropdown similar to a <select>, see useSelect.

  • Exposed to assistive technology as a listbox using ARIA
  • Support for single, multiple, or no selection
  • Support for disabled items
  • Support for sections
  • Labeling support for accessibility
  • Support for mouse, touch, and keyboard interactions
  • Tab stop focus management
  • Keyboard navigation support including arrow keys, home/end, page up/down, select all, and clear
  • Automatic scrolling support during keyboard navigation
  • Typeahead to allow focusing options by typing text
  • Virtualized scrolling support for performance with long lists

Anatomy#


Listbox anatomy diagramOptionLabelOption labelOption descriptionListboxSection headingLabelOption 1Option 2DescriptionDescriptionOption 3DescriptionSECTION TITLEGroup

A listbox consists of a container element, with a list of options or groups inside. useListBox, useOption, and useListBoxSection handle exposing this to assistive technology using ARIA, along with handling keyboard, mouse, and interactions to support selection and focus behavior.

useListBox returns props that you should spread onto the list container element, along with props for an optional visual label:

NameTypeDescription
listBoxPropsHTMLAttributes<HTMLElement>Props for the listbox element.
labelPropsHTMLAttributes<HTMLElement>Props for the listbox's visual label element (if any).

useOption returns props for an individual option and its children:

NameTypeDescription
optionPropsHTMLAttributes<HTMLElement>Props for the option element.
labelPropsHTMLAttributes<HTMLElement>Props for the main text element inside the option.
descriptionPropsHTMLAttributes<HTMLElement>Props for the description text element inside the option, if any.
isFocusedbooleanWhether the option is currently focused.
isSelectedbooleanWhether the option is currently selected.
isPressedbooleanWhether the option is currently in a pressed state.
isDisabledbooleanWhether the option is disabled.

useListBoxSection returns props for a section:

NameTypeDescription
itemPropsHTMLAttributes<HTMLElement>Props for the wrapper list item.
headingPropsHTMLAttributes<HTMLElement>Props for the heading element, if any.
groupPropsHTMLAttributes<HTMLElement>Props for the group element.

State is managed by the useListState hook from @react-stately/list. The state object should be passed as an option to each of the above hooks.

If a listbox, options, or group does not have a visible label, an aria-label or aria-labelledby prop must be passed instead to identify the element to assistive technology.

State management#


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

In addition, useListState 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. For more information, see Selection.

Example#


This example uses HTML <ul> and <li> elements to represent the list, and applies props from useListBox and useOption.

import {useListState} from '@react-stately/list';
import {Item} from '@react-stately/collections';
import {useFocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';

function ListBox(props) {
  // Create state based on the incoming props
  let state = useListState(props);

  // Get props for the listbox element
  let ref = React.useRef();
  let {listBoxProps, labelProps} = useListBox(props, state, ref);

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul
        {...listBoxProps}
        ref={ref}
        style={{
          padding: 0,
          margin: '5px 0',
          listStyle: 'none',
          border: '1px solid gray',
          maxWidth: 250
        }}>
        {[...state.collection].map((item) => (
          <Option key={item.key} item={item} state={state} />
        ))}
      </ul>
    </>
  );
}

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
    },
    state,
    ref
  );

  // Determine whether we should show a keyboard
  // focus ring for accessibility
  let {isFocusVisible, focusProps} = useFocusRing();

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      style={{
        background: isSelected ? 'blueviolet' : 'transparent',
        color: isSelected ? 'white' : null,
        padding: '2px 5px',
        outline: isFocusVisible ? '2px solid orange' : 'none'
      }}>
      {item.rendered}
    </li>
  );
}

<ListBox label="Choose an option" selectionMode="single">
  <Item>One</Item>
  <Item>Two</Item>
  <Item>Three</Item>
</ListBox>
import {useListState} from '@react-stately/list';
import {Item} from '@react-stately/collections';
import {useFocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';

function ListBox(props) {
  // Create state based on the incoming props
  let state = useListState(props);

  // Get props for the listbox element
  let ref = React.useRef();
  let {listBoxProps, labelProps} = useListBox(
    props,
    state,
    ref
  );

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul
        {...listBoxProps}
        ref={ref}
        style={{
          padding: 0,
          margin: '5px 0',
          listStyle: 'none',
          border: '1px solid gray',
          maxWidth: 250
        }}>
        {[...state.collection].map((item) => (
          <Option
            key={item.key}
            item={item}
            state={state}
          />
        ))}
      </ul>
    </>
  );
}

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
    },
    state,
    ref
  );

  // Determine whether we should show a keyboard
  // focus ring for accessibility
  let {isFocusVisible, focusProps} = useFocusRing();

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      style={{
        background: isSelected
          ? 'blueviolet'
          : 'transparent',
        color: isSelected ? 'white' : null,
        padding: '2px 5px',
        outline: isFocusVisible
          ? '2px solid orange'
          : 'none'
      }}>
      {item.rendered}
    </li>
  );
}

<ListBox label="Choose an option" selectionMode="single">
  <Item>One</Item>
  <Item>Two</Item>
  <Item>Three</Item>
</ListBox>
import {useListState} from '@react-stately/list';
import {Item} from '@react-stately/collections';
import {useFocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';

function ListBox(props) {
  // Create state based on the incoming props
  let state = useListState(
    props
  );

  // Get props for the listbox element
  let ref = React.useRef();
  let {
    listBoxProps,
    labelProps
  } = useListBox(
    props,
    state,
    ref
  );

  return (
    <>
      <div
        {...labelProps}>
        {props.label}
      </div>
      <ul
        {...listBoxProps}
        ref={ref}
        style={{
          padding: 0,
          margin:
            '5px 0',
          listStyle:
            'none',
          border:
            '1px solid gray',
          maxWidth: 250
        }}>
        {[
          ...state.collection
        ].map((item) => (
          <Option
            key={
              item.key
            }
            item={item}
            state={state}
          />
        ))}
      </ul>
    </>
  );
}

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
    },
    state,
    ref
  );

  // Determine whether we should show a keyboard
  // focus ring for accessibility
  let {
    isFocusVisible,
    focusProps
  } = useFocusRing();

  return (
    <li
      {...mergeProps(
        optionProps,
        focusProps
      )}
      ref={ref}
      style={{
        background: isSelected
          ? 'blueviolet'
          : 'transparent',
        color: isSelected
          ? 'white'
          : null,
        padding:
          '2px 5px',
        outline: isFocusVisible
          ? '2px solid orange'
          : 'none'
      }}>
      {item.rendered}
    </li>
  );
}

<ListBox
  label="Choose an option"
  selectionMode="single">
  <Item>One</Item>
  <Item>Two</Item>
  <Item>Three</Item>
</ListBox>

Sections#


This example shows how a listbox can support sections with separators and headings using props from useListBoxSection. This is accomplished using four extra elements: an <li> between the sections to represent the separator, an <li> to contain the heading <span> element, and a <ul> to contain the child items. This structure is necessary to ensure HTML semantics are correct.

import {Section} from '@react-stately/collections';
import {useSeparator} from '@react-aria/separator';

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

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul
        {...listBoxProps}
        ref={ref}
        style={{
          padding: 0,
          margin: '5px 0',
          listStyle: 'none',
          border: '1px solid gray',
          maxWidth: 250
        }}>
        {[...state.collection].map((item) => (
          <ListBoxSection key={item.key} section={item} state={state} />
        ))}
      </ul>
    </>
  );
}

function ListBoxSection({section, state}) {
  let {itemProps, headingProps, groupProps} = useListBoxSection({
    heading: section.rendered,
    'aria-label': section['aria-label']
  });

  let {separatorProps} = useSeparator({
    elementType: 'li'
  });

  // If the section is not the first, add a separator element.
  // The heading is rendered inside an <li> element, which contains
  // a <ul> with the child items.
  return (
    <>
      {section.key !== state.collection.getFirstKey() && (
        <li
          {...separatorProps}
          style={{
            borderTop: '1px solid gray',
            margin: '2px 5px'
          }}
        />
      )}
      <li {...itemProps}>
        {section.rendered && (
          <span
            {...headingProps}
            style={{
              fontWeight: 'bold',
              fontSize: '1.1em',
              padding: '2px 5px'
            }}>
            {section.rendered}
          </span>
        )}
        <ul
          {...groupProps}
          style={{
            padding: 0,
            listStyle: 'none'
          }}>
          {[...section.childNodes].map((node) => (
            <Option key={node.key} item={node} state={state} />
          ))}
        </ul>
      </li>
    </>
  );
}

function Option({item, state}) {
  // Same as in the first example...
}

<ListBox label="Choose an option" selectionMode="multiple">
  <Section title="Section 1">
    <Item>One</Item>
    <Item>Two</Item>
    <Item>Three</Item>
  </Section>
  <Section title="Section 2">
    <Item>One</Item>
    <Item>Two</Item>
    <Item>Three</Item>
  </Section>
</ListBox>
import {Section} from '@react-stately/collections';
import {useSeparator} from '@react-aria/separator';

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

  return (
    <>
      <div {...labelProps}>{props.label}</div>
      <ul
        {...listBoxProps}
        ref={ref}
        style={{
          padding: 0,
          margin: '5px 0',
          listStyle: 'none',
          border: '1px solid gray',
          maxWidth: 250
        }}>
        {[...state.collection].map((item) => (
          <ListBoxSection
            key={item.key}
            section={item}
            state={state}
          />
        ))}
      </ul>
    </>
  );
}

function ListBoxSection({section, state}) {
  let {
    itemProps,
    headingProps,
    groupProps
  } = useListBoxSection({
    heading: section.rendered,
    'aria-label': section['aria-label']
  });

  let {separatorProps} = useSeparator({
    elementType: 'li'
  });

  // If the section is not the first, add a separator element.
  // The heading is rendered inside an <li> element, which contains
  // a <ul> with the child items.
  return (
    <>
      {section.key !== state.collection.getFirstKey() && (
        <li
          {...separatorProps}
          style={{
            borderTop: '1px solid gray',
            margin: '2px 5px'
          }}
        />
      )}
      <li {...itemProps}>
        {section.rendered && (
          <span
            {...headingProps}
            style={{
              fontWeight: 'bold',
              fontSize: '1.1em',
              padding: '2px 5px'
            }}>
            {section.rendered}
          </span>
        )}
        <ul
          {...groupProps}
          style={{
            padding: 0,
            listStyle: 'none'
          }}>
          {[...section.childNodes].map((node) => (
            <Option
              key={node.key}
              item={node}
              state={state}
            />
          ))}
        </ul>
      </li>
    </>
  );
}

function Option({item, state}) {
  // Same as in the first example...
}

<ListBox
  label="Choose an option"
  selectionMode="multiple">
  <Section title="Section 1">
    <Item>One</Item>
    <Item>Two</Item>
    <Item>Three</Item>
  </Section>
  <Section title="Section 2">
    <Item>One</Item>
    <Item>Two</Item>
    <Item>Three</Item>
  </Section>
</ListBox>
import {Section} from '@react-stately/collections';
import {useSeparator} from '@react-aria/separator';

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

  return (
    <>
      <div
        {...labelProps}>
        {props.label}
      </div>
      <ul
        {...listBoxProps}
        ref={ref}
        style={{
          padding: 0,
          margin:
            '5px 0',
          listStyle:
            'none',
          border:
            '1px solid gray',
          maxWidth: 250
        }}>
        {[
          ...state.collection
        ].map((item) => (
          <ListBoxSection
            key={
              item.key
            }
            section={
              item
            }
            state={state}
          />
        ))}
      </ul>
    </>
  );
}

function ListBoxSection({
  section,
  state
}) {
  let {
    itemProps,
    headingProps,
    groupProps
  } = useListBoxSection({
    heading:
      section.rendered,
    'aria-label':
      section[
        'aria-label'
      ]
  });

  let {
    separatorProps
  } = useSeparator({
    elementType: 'li'
  });

  // If the section is not the first, add a separator element.
  // The heading is rendered inside an <li> element, which contains
  // a <ul> with the child items.
  return (
    <>
      {section.key !==
        state.collection.getFirstKey() && (
        <li
          {...separatorProps}
          style={{
            borderTop:
              '1px solid gray',
            margin:
              '2px 5px'
          }}
        />
      )}
      <li {...itemProps}>
        {section.rendered && (
          <span
            {...headingProps}
            style={{
              fontWeight:
                'bold',
              fontSize:
                '1.1em',
              padding:
                '2px 5px'
            }}>
            {
              section.rendered
            }
          </span>
        )}
        <ul
          {...groupProps}
          style={{
            padding: 0,
            listStyle:
              'none'
          }}>
          {[
            ...section.childNodes
          ].map(
            (node) => (
              <Option
                key={
                  node.key
                }
                item={
                  node
                }
                state={
                  state
                }
              />
            )
          )}
        </ul>
      </li>
    </>
  );
}

function Option({
  item,
  state
}) {
  // Same as in the first example...
}

<ListBox
  label="Choose an option"
  selectionMode="multiple">
  <Section title="Section 1">
    <Item>One</Item>
    <Item>Two</Item>
    <Item>Three</Item>
  </Section>
  <Section title="Section 2">
    <Item>One</Item>
    <Item>Two</Item>
    <Item>Three</Item>
  </Section>
</ListBox>

Complex options#


By default, options that only contain text will be labeled by the contents of the option. For options that have more complex content (e.g. icons, multiple lines of text, etc.), use labelProps and descriptionProps from useOption as needed to apply to the main text element of the option and its description. This improves screen reader announcement.

NOTE: listbox options cannot contain interactive content (e.g. buttons, checkboxes, etc.). For these cases, see useGrid instead.

This example shows how labelProps and descriptionProps can be applied to child elements of the item to apply ARIA properties returned by useOption. This is done using React.cloneElement in this example, but you can use context or other approaches for this as well.

function ListBox(props) {
  // Same as the first example...
}

function Option({item, state}) {
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);
  let isSelected = state.selectionManager.isSelected(item.key);
  let {optionProps, labelProps, descriptionProps} = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected
    },
    state,
    ref
  );
  let {isFocusVisible, focusProps} = useFocusRing();

  // Pull out the two expected children. We will clone them
  // and add the necessary props for accessibility.
  let [title, description] = item.rendered;

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      style={{
        background: isSelected ? 'blueviolet' : 'transparent',
        color: isSelected ? 'white' : null,
        padding: '2px 5px',
        outline: isFocusVisible ? '2px solid orange' : 'none'
      }}>
      {React.cloneElement(title, labelProps)}
      {React.cloneElement(description, descriptionProps)}
    </li>
  );
}

<ListBox label="Text alignment" selectionMode="multiple">
  <Item textValue="Align Left">
    <div>
      <strong>Align Left</strong>
    </div>
    <div>Align the selected text to the left</div>
  </Item>
  <Item textValue="Align Center">
    <div>
      <strong>Align Center</strong>
    </div>
    <div>Align the selected text center</div>
  </Item>
  <Item textValue="Align Right">
    <div>
      <strong>Align Right</strong>
    </div>
    <div>Align the selected text to the right</div>
  </Item>
</ListBox>
function ListBox(props) {
  // Same as the first example...
}

function Option({item, state}) {
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(item.key);
  let isSelected = state.selectionManager.isSelected(
    item.key
  );
  let {
    optionProps,
    labelProps,
    descriptionProps
  } = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected
    },
    state,
    ref
  );
  let {isFocusVisible, focusProps} = useFocusRing();

  // Pull out the two expected children. We will clone them
  // and add the necessary props for accessibility.
  let [title, description] = item.rendered;

  return (
    <li
      {...mergeProps(optionProps, focusProps)}
      ref={ref}
      style={{
        background: isSelected
          ? 'blueviolet'
          : 'transparent',
        color: isSelected ? 'white' : null,
        padding: '2px 5px',
        outline: isFocusVisible
          ? '2px solid orange'
          : 'none'
      }}>
      {React.cloneElement(title, labelProps)}
      {React.cloneElement(description, descriptionProps)}
    </li>
  );
}

<ListBox label="Text alignment" selectionMode="multiple">
  <Item textValue="Align Left">
    <div>
      <strong>Align Left</strong>
    </div>
    <div>Align the selected text to the left</div>
  </Item>
  <Item textValue="Align Center">
    <div>
      <strong>Align Center</strong>
    </div>
    <div>Align the selected text center</div>
  </Item>
  <Item textValue="Align Right">
    <div>
      <strong>Align Right</strong>
    </div>
    <div>Align the selected text to the right</div>
  </Item>
</ListBox>
function ListBox(props) {
  // Same as the first example...
}

function Option({
  item,
  state
}) {
  let ref = React.useRef();
  let isDisabled = state.disabledKeys.has(
    item.key
  );
  let isSelected = state.selectionManager.isSelected(
    item.key
  );
  let {
    optionProps,
    labelProps,
    descriptionProps
  } = useOption(
    {
      key: item.key,
      isDisabled,
      isSelected
    },
    state,
    ref
  );
  let {
    isFocusVisible,
    focusProps
  } = useFocusRing();

  // Pull out the two expected children. We will clone them
  // and add the necessary props for accessibility.
  let [
    title,
    description
  ] = item.rendered;

  return (
    <li
      {...mergeProps(
        optionProps,
        focusProps
      )}
      ref={ref}
      style={{
        background: isSelected
          ? 'blueviolet'
          : 'transparent',
        color: isSelected
          ? 'white'
          : null,
        padding:
          '2px 5px',
        outline: isFocusVisible
          ? '2px solid orange'
          : 'none'
      }}>
      {React.cloneElement(
        title,
        labelProps
      )}
      {React.cloneElement(
        description,
        descriptionProps
      )}
    </li>
  );
}

<ListBox
  label="Text alignment"
  selectionMode="multiple">
  <Item textValue="Align Left">
    <div>
      <strong>
        Align Left
      </strong>
    </div>
    <div>
      Align the
      selected text to
      the left
    </div>
  </Item>
  <Item textValue="Align Center">
    <div>
      <strong>
        Align Center
      </strong>
    </div>
    <div>
      Align the
      selected text
      center
    </div>
  </Item>
  <Item textValue="Align Right">
    <div>
      <strong>
        Align Right
      </strong>
    </div>
    <div>
      Align the
      selected text to
      the right
    </div>
  </Item>
</ListBox>

Internationalization#


useListBox handles 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 listbox.

RTL#

In right-to-left languages, the listbox options should be mirrored. The text content should be aligned to the right. Ensure that your CSS accounts for this.