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
version3.23.1
usageimport {useListBox, useOption, useListBoxSection} from 'react-aria'

API#


useListBox<T>( props: AriaListBoxOptions<T>, state: ListState<T>, ref: RefObject<HTMLElement> ): ListBoxAria useOption<T>( props: AriaOptionProps, state: ListState<T>, ref: RefObject<FocusableElement> ): OptionAria useListBoxSection( (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#


LabelOption 1Option 2OptionLabelOption labelDescriptionDescriptionOption 3DescriptionOption descriptionListboxSECTION TITLESection headingGroup

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
listBoxPropsDOMAttributesProps for the listbox element.
labelPropsDOMAttributesProps for the listbox's visual label element (if any).

useOption returns props for an individual option and its children, along with states you can use for styling:

NameTypeDescription
optionPropsDOMAttributesProps for the option element.
labelPropsDOMAttributesProps for the main text element inside the option.
descriptionPropsDOMAttributesProps for the description text element inside the option, if any.
isFocusedbooleanWhether the option is currently focused.
isFocusVisiblebooleanWhether the option is keyboard focused.
isPressedbooleanWhether the item is currently in a pressed state.
isSelectedbooleanWhether the item is currently selected.
isDisabledboolean

Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on disabledKeys and disabledBehavior.

allowsSelectionbooleanWhether the item may be selected, dependent on selectionMode, disabledKeys, and disabledBehavior.
hasActionboolean

Whether the item has an action, dependent on onAction, disabledKeys, and disabledBehavior. It may also change depending on the current selection state of the list (e.g. when selection is primary). This can be used to enable or disable hover styles or other visual indications of interactivity.

useListBoxSection returns props for a section:

NameTypeDescription
itemPropsDOMAttributesProps for the wrapper list item.
headingPropsDOMAttributesProps for the heading element, if any.
groupPropsDOMAttributesProps 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. For each item in the collection in state, either an Option or ListBoxSection (defined below) is rendered according to the item's type property.

import type {AriaListBoxProps} from 'react-aria';
import {Item, useListState} from 'react-stately';
import {mergeProps, useFocusRing, useListBox, useOption} from 'react-aria';

function ListBox<T extends object>(props: AriaListBoxProps<T>) {
  // Create state based on the incoming props
  let state = useListState(props);

  // Get props for the listbox element
  let ref = React.useRef(null);
  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,
          maxHeight: 300,
          overflow: 'auto'
        }}
      >
        {[...state.collection].map((item) => (
          item.type === 'section'
            ? <ListBoxSection key={item.key} section={item} state={state} />
            : <Option key={item.key} item={item} state={state} />
        ))}
      </ul>
    </>
  );
}

function Option({ item, state }) {
  // Get props for the option element
  let ref = React.useRef(null);
  let { optionProps, isSelected, isDisabled } = useOption(
    { key: item.key },
    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: isDisabled ? '#aaa' : isSelected ? 'white' : null,
        padding: '2px 5px',
        outline: isFocusVisible ? '2px solid orange' : 'none'
      }}
    >
      {item.rendered}
    </li>
  );
}

<ListBox label="Alignment" selectionMode="single">
  <Item>Left</Item>
  <Item>Middle</Item>
  <Item>Right</Item>
</ListBox>
import type {AriaListBoxProps} from 'react-aria';
import {Item, useListState} from 'react-stately';
import {
  mergeProps,
  useFocusRing,
  useListBox,
  useOption
} from 'react-aria';

function ListBox<T extends object>(
  props: AriaListBoxProps<T>
) {
  // Create state based on the incoming props
  let state = useListState(props);

  // Get props for the listbox element
  let ref = React.useRef(null);
  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,
          maxHeight: 300,
          overflow: 'auto'
        }}
      >
        {[...state.collection].map((item) => (
          item.type === 'section'
            ? (
              <ListBoxSection
                key={item.key}
                section={item}
                state={state}
              />
            )
            : (
              <Option
                key={item.key}
                item={item}
                state={state}
              />
            )
        ))}
      </ul>
    </>
  );
}

function Option({ item, state }) {
  // Get props for the option element
  let ref = React.useRef(null);
  let { optionProps, isSelected, isDisabled } = useOption(
    { key: item.key },
    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: isDisabled
          ? '#aaa'
          : isSelected
          ? 'white'
          : null,
        padding: '2px 5px',
        outline: isFocusVisible
          ? '2px solid orange'
          : 'none'
      }}
    >
      {item.rendered}
    </li>
  );
}

<ListBox label="Alignment" selectionMode="single">
  <Item>Left</Item>
  <Item>Middle</Item>
  <Item>Right</Item>
</ListBox>
import type {AriaListBoxProps} from 'react-aria';
import {
  Item,
  useListState
} from 'react-stately';
import {
  mergeProps,
  useFocusRing,
  useListBox,
  useOption
} from 'react-aria';

function ListBox<
  T extends object
>(
  props:
    AriaListBoxProps<T>
) {
  // Create state based on the incoming props
  let state =
    useListState(props);

  // Get props for the listbox element
  let ref = React.useRef(
    null
  );
  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,
          maxHeight: 300,
          overflow:
            'auto'
        }}
      >
        {[
          ...state
            .collection
        ].map((item) => (
          item.type ===
              'section'
            ? (
              <ListBoxSection
                key={item
                  .key}
                section={item}
                state={state}
              />
            )
            : (
              <Option
                key={item
                  .key}
                item={item}
                state={state}
              />
            )
        ))}
      </ul>
    </>
  );
}

function Option(
  { item, state }
) {
  // Get props for the option element
  let ref = React.useRef(
    null
  );
  let {
    optionProps,
    isSelected,
    isDisabled
  } = useOption(
    { key: item.key },
    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: isDisabled
          ? '#aaa'
          : isSelected
          ? 'white'
          : null,
        padding:
          '2px 5px',
        outline:
          isFocusVisible
            ? '2px solid orange'
            : 'none'
      }}
    >
      {item.rendered}
    </li>
  );
}

<ListBox
  label="Alignment"
  selectionMode="single"
>
  <Item>Left</Item>
  <Item>Middle</Item>
  <Item>Right</Item>
</ListBox>

Dynamic collections#


ListBox follows the Collection Components API, accepting both static and dynamic collections. The example above shows 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 ListBox using the items 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: 'Aardvark' },
    { id: 2, name: 'Cat' },
    { id: 3, name: 'Dog' },
    { id: 4, name: 'Kangaroo' },
    { id: 5, name: 'Koala' },
    { id: 6, name: 'Penguin' },
    { id: 7, name: 'Snake' },
    { id: 8, name: 'Turtle' },
    { id: 9, name: 'Wombat' }
  ];

  return (
    <ListBox label="Animals" items={options} selectionMode="single">
      {(item) => <Item>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  let options = [
    { id: 1, name: 'Aardvark' },
    { id: 2, name: 'Cat' },
    { id: 3, name: 'Dog' },
    { id: 4, name: 'Kangaroo' },
    { id: 5, name: 'Koala' },
    { id: 6, name: 'Penguin' },
    { id: 7, name: 'Snake' },
    { id: 8, name: 'Turtle' },
    { id: 9, name: 'Wombat' }
  ];

  return (
    <ListBox
      label="Animals"
      items={options}
      selectionMode="single"
    >
      {(item) => <Item>{item.name}</Item>}
    </ListBox>
  );
}
function Example() {
  let options = [
    {
      id: 1,
      name: 'Aardvark'
    },
    {
      id: 2,
      name: 'Cat'
    },
    {
      id: 3,
      name: 'Dog'
    },
    {
      id: 4,
      name: 'Kangaroo'
    },
    {
      id: 5,
      name: 'Koala'
    },
    {
      id: 6,
      name: 'Penguin'
    },
    {
      id: 7,
      name: 'Snake'
    },
    {
      id: 8,
      name: 'Turtle'
    },
    {
      id: 9,
      name: 'Wombat'
    }
  ];

  return (
    <ListBox
      label="Animals"
      items={options}
      selectionMode="single"
    >
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </ListBox>
  );
}

Selection#


ListBox supports multiple selection modes. By default, selection is disabled, however this can be changed using the selectionMode prop. Use defaultSelectedKeys to provide a default set of selected items (uncontrolled) and selectedKeys to set the selected items (controlled). The value of the selected keys must match the key prop of the items. See the react-stately Selection docs for more details.

import type {Selection} from 'react-stately';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(new Set(['cheese']));

  return (
    <>
      <ListBox
        label="Choose sandwich contents"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item key="lettuce">Lettuce</Item>
        <Item key="tomato">Tomato</Item>
        <Item key="cheese">Cheese</Item>
        <Item key="tuna">Tuna Salad</Item>
        <Item key="egg">Egg Salad</Item>
        <Item key="ham">Ham</Item>
      </ListBox>
      <p>
        Current selection (controlled):{' '}
        {selected === 'all' ? 'all' : [...selected].join(', ')}
      </p>
    </>
  );
}
import type {Selection} from 'react-stately';

function Example() {
  let [selected, setSelected] = React.useState<Selection>(
    new Set(['cheese'])
  );

  return (
    <>
      <ListBox
        label="Choose sandwich contents"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item key="lettuce">Lettuce</Item>
        <Item key="tomato">Tomato</Item>
        <Item key="cheese">Cheese</Item>
        <Item key="tuna">Tuna Salad</Item>
        <Item key="egg">Egg Salad</Item>
        <Item key="ham">Ham</Item>
      </ListBox>
      <p>
        Current selection (controlled): {selected === 'all'
          ? 'all'
          : [...selected].join(', ')}
      </p>
    </>
  );
}
import type {Selection} from 'react-stately';

function Example() {
  let [
    selected,
    setSelected
  ] = React.useState<
    Selection
  >(new Set(['cheese']));

  return (
    <>
      <ListBox
        label="Choose sandwich contents"
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
      >
        <Item key="lettuce">
          Lettuce
        </Item>
        <Item key="tomato">
          Tomato
        </Item>
        <Item key="cheese">
          Cheese
        </Item>
        <Item key="tuna">
          Tuna Salad
        </Item>
        <Item key="egg">
          Egg Salad
        </Item>
        <Item key="ham">
          Ham
        </Item>
      </ListBox>
      <p>
        Current selection
        (controlled):
        {' '}
        {selected ===
            'all'
          ? 'all'
          : [...selected]
            .join(', ')}
      </p>
    </>
  );
}

Selection behavior#

By default, useListBox uses the "toggle" selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection.

When selectionBehavior is set to "replace", clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. On touch screen devices, selection always behaves as toggle since modifier keys may not be available.

These selection behaviors are defined in Aria Practices.

<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
  selectionBehavior="replace"
>
  <Item key="lettuce">Lettuce</Item>
  <Item key="tomato">Tomato</Item>
  <Item key="cheese">Cheese</Item>
  <Item key="tuna">Tuna Salad</Item>
  <Item key="egg">Egg Salad</Item>
  <Item key="ham">Ham</Item>
</ListBox>
<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
  selectionBehavior="replace"
>
  <Item key="lettuce">Lettuce</Item>
  <Item key="tomato">Tomato</Item>
  <Item key="cheese">Cheese</Item>
  <Item key="tuna">Tuna Salad</Item>
  <Item key="egg">Egg Salad</Item>
  <Item key="ham">Ham</Item>
</ListBox>
<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
  selectionBehavior="replace"
>
  <Item key="lettuce">
    Lettuce
  </Item>
  <Item key="tomato">
    Tomato
  </Item>
  <Item key="cheese">
    Cheese
  </Item>
  <Item key="tuna">
    Tuna Salad
  </Item>
  <Item key="egg">
    Egg Salad
  </Item>
  <Item key="ham">
    Ham
  </Item>
</ListBox>

Sections#


ListBox supports sections with separators and headings in order to group options. Sections can be used by wrapping groups of Items in a Section component. Each Section takes a title and key prop. To implement sections, implement the ListBoxSection component referenced above using the useListBoxSection hook. It will include 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 {useListBoxSection, useSeparator} from 'react-aria';

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>
    </>
  );
}
import {useListBoxSection, useSeparator} from 'react-aria';

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>
    </>
  );
}
import {
  useListBoxSection,
  useSeparator
} from 'react-aria';

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>
    </>
  );
}

Static items#

With this in place, we can now render a static ListBox with multiple sections:

import {Section} from 'react-stately';

<ListBox label="Choose sandwich contents" selectionMode="multiple">
  <Section title="Veggies">
    <Item key="lettuce">Lettuce</Item>
    <Item key="tomato">Tomato</Item>
    <Item key="onion">Onion</Item>
  </Section>
  <Section title="Protein">
    <Item key="ham">Ham</Item>
    <Item key="tuna">Tuna</Item>
    <Item key="tofu">Tofu</Item>
  </Section>
  <Section title="Condiments">
    <Item key="mayo">Mayonaise</Item>
    <Item key="mustard">Mustard</Item>
    <Item key="ranch">Ranch</Item>
  </Section>
</ListBox>
import {Section} from 'react-stately';

<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
>
  <Section title="Veggies">
    <Item key="lettuce">Lettuce</Item>
    <Item key="tomato">Tomato</Item>
    <Item key="onion">Onion</Item>
  </Section>
  <Section title="Protein">
    <Item key="ham">Ham</Item>
    <Item key="tuna">Tuna</Item>
    <Item key="tofu">Tofu</Item>
  </Section>
  <Section title="Condiments">
    <Item key="mayo">Mayonaise</Item>
    <Item key="mustard">Mustard</Item>
    <Item key="ranch">Ranch</Item>
  </Section>
</ListBox>
import {Section} from 'react-stately';

<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
>
  <Section title="Veggies">
    <Item key="lettuce">
      Lettuce
    </Item>
    <Item key="tomato">
      Tomato
    </Item>
    <Item key="onion">
      Onion
    </Item>
  </Section>
  <Section title="Protein">
    <Item key="ham">
      Ham
    </Item>
    <Item key="tuna">
      Tuna
    </Item>
    <Item key="tofu">
      Tofu
    </Item>
  </Section>
  <Section title="Condiments">
    <Item key="mayo">
      Mayonaise
    </Item>
    <Item key="mustard">
      Mustard
    </Item>
    <Item key="ranch">
      Ranch
    </Item>
  </Section>
</ListBox>

Dynamic items#

The above example shows sections with static items. Sections can also be populated from a heirarchical data structure. Similarly to the props on ListBox, <Section> takes an array of data using the items prop.

import type {Selection} from 'react-stately';

function Example() {
  let options = [
    {name: 'Australian', children: [
      {id: 2, name: 'Koala'},
      {id: 3, name: 'Kangaroo'},
      {id: 4, name: 'Platypus'}
    ]},
    {name: 'American', children: [
      {id: 6, name: 'Bald Eagle'},
      {id: 7, name: 'Bison'},
      {id: 8, name: 'Skunk'}
    ]}
  ];
  let [selected, setSelected] = React.useState<Selection>(new Set());

  return (
    <ListBox
      label="Pick an animal"
      items={options}
      selectedKeys={selected}
      selectionMode="single"
      onSelectionChange={setSelected}>
      {item => (
        <Section key={item.name} items={item.children} title={item.name}>
          {item => <Item>{item.name}</Item>}
        </Section>
      )}
    </ListBox>
  );
}
import type {Selection} from 'react-stately';

function Example() {
  let options = [
    {
      name: 'Australian',
      children: [
        { id: 2, name: 'Koala' },
        { id: 3, name: 'Kangaroo' },
        { id: 4, name: 'Platypus' }
      ]
    },
    {
      name: 'American',
      children: [
        { id: 6, name: 'Bald Eagle' },
        { id: 7, name: 'Bison' },
        { id: 8, name: 'Skunk' }
      ]
    }
  ];
  let [selected, setSelected] = React.useState<Selection>(
    new Set()
  );

  return (
    <ListBox
      label="Pick an animal"
      items={options}
      selectedKeys={selected}
      selectionMode="single"
      onSelectionChange={setSelected}
    >
      {(item) => (
        <Section
          key={item.name}
          items={item.children}
          title={item.name}
        >
          {(item) => <Item>{item.name}</Item>}
        </Section>
      )}
    </ListBox>
  );
}
import type {Selection} from 'react-stately';

function Example() {
  let options = [
    {
      name: 'Australian',
      children: [
        {
          id: 2,
          name: 'Koala'
        },
        {
          id: 3,
          name:
            'Kangaroo'
        },
        {
          id: 4,
          name:
            'Platypus'
        }
      ]
    },
    {
      name: 'American',
      children: [
        {
          id: 6,
          name:
            'Bald Eagle'
        },
        {
          id: 7,
          name: 'Bison'
        },
        {
          id: 8,
          name: 'Skunk'
        }
      ]
    }
  ];
  let [
    selected,
    setSelected
  ] = React.useState<
    Selection
  >(new Set());

  return (
    <ListBox
      label="Pick an animal"
      items={options}
      selectedKeys={selected}
      selectionMode="single"
      onSelectionChange={setSelected}
    >
      {(item) => (
        <Section
          key={item.name}
          items={item
            .children}
          title={item
            .name}
        >
          {(item) => (
            <Item>
              {item.name}
            </Item>
          )}
        </Section>
      )}
    </ListBox>
  );
}

Accessibility#

Sections without a title must provide an aria-label for accessibility.

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 useGridList instead.

To implement this, we'll update the Option component to apply the ARIA properties returned by useOption to the appropriate elements. In this example, we'll pull them out of props.children and use React.cloneElement to apply the props, but you may want to use a more robust approach (e.g. context).

function Option({ item, state }) {
  let ref = React.useRef(null);
  let { optionProps, labelProps, descriptionProps, isSelected } = useOption(
    { key: item.key },
    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="single">
  <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 Option({ item, state }) {
  let ref = React.useRef(null);
  let {
    optionProps,
    labelProps,
    descriptionProps,
    isSelected
  } = useOption({ key: item.key }, 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="single">
  <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 Option(
  { item, state }
) {
  let ref = React.useRef(
    null
  );
  let {
    optionProps,
    labelProps,
    descriptionProps,
    isSelected
  } = useOption(
    { key: item.key },
    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="single"
>
  <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>

Asynchronous loading#


This example uses the useAsyncList hook to handle asynchronous loading 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';

interface Pokemon {
  name: string;
}

function AsyncLoadingExample() {
  let list = useAsyncList<Pokemon>({
    async load({ signal }) {
      let res = await fetch(
        `https://pokeapi.co/api/v2/pokemon`,
        { signal }
      );
      let json = await res.json();

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

  return (
    <ListBox label="Pick a Pokemon" items={list.items} selectionMode="single">
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </ListBox>
  );
}
import {useAsyncList} from 'react-stately';

interface Pokemon {
  name: string;
}

function AsyncLoadingExample() {
  let list = useAsyncList<Pokemon>({
    async load({ signal }) {
      let res = await fetch(
        `https://pokeapi.co/api/v2/pokemon`,
        { signal }
      );
      let json = await res.json();

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

  return (
    <ListBox
      label="Pick a Pokemon"
      items={list.items}
      selectionMode="single"
    >
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </ListBox>
  );
}
import {useAsyncList} from 'react-stately';

interface Pokemon {
  name: string;
}

function AsyncLoadingExample() {
  let list =
    useAsyncList<
      Pokemon
    >({
      async load(
        { signal }
      ) {
        let res =
          await fetch(
            `https://pokeapi.co/api/v2/pokemon`,
            { signal }
          );
        let json =
          await res
            .json();

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

  return (
    <ListBox
      label="Pick a Pokemon"
      items={list.items}
      selectionMode="single"
    >
      {(item) => (
        <Item
          key={item.name}
        >
          {item.name}
        </Item>
      )}
    </ListBox>
  );
}

Disabled items#


useListBox supports marking items as disabled using the disabledKeys prop. Each key in this list corresponds with the key prop passed to the Item component, or automatically derived from the values passed to the items prop. See Collections for more details.

Disabled items are not focusable, selectable, or keyboard navigable. The isDisabled property returned by useOption can be used to style the item appropriately.

<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
  disabledKeys={['tuna']}
>
  <Item key="lettuce">Lettuce</Item>
  <Item key="tomato">Tomato</Item>
  <Item key="cheese">Cheese</Item>
  <Item key="tuna">Tuna Salad</Item>
  <Item key="egg">Egg Salad</Item>
  <Item key="ham">Ham</Item>
</ListBox>
<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
  disabledKeys={['tuna']}
>
  <Item key="lettuce">Lettuce</Item>
  <Item key="tomato">Tomato</Item>
  <Item key="cheese">Cheese</Item>
  <Item key="tuna">Tuna Salad</Item>
  <Item key="egg">Egg Salad</Item>
  <Item key="ham">Ham</Item>
</ListBox>
<ListBox
  label="Choose sandwich contents"
  selectionMode="multiple"
  disabledKeys={[
    'tuna'
  ]}
>
  <Item key="lettuce">
    Lettuce
  </Item>
  <Item key="tomato">
    Tomato
  </Item>
  <Item key="cheese">
    Cheese
  </Item>
  <Item key="tuna">
    Tuna Salad
  </Item>
  <Item key="egg">
    Egg Salad
  </Item>
  <Item key="ham">
    Ham
  </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.