useGridList

Provides the behavior and accessibility implementation for a list component with interactive children. A grid list displays data in a single column and enables a user to navigate its contents via directional navigation keys.

installyarn add react-aria
version3.35.0
usageimport {useGridList, useGridListItem, useGridListSelectionCheckbox} from 'react-aria'

API#


useGridList<T>( props: AriaGridListOptions<T>, state: ListState<T>, ref: RefObject<HTMLElementnull> ): GridListAria useGridListItem<T>( props: AriaGridListItemOptions, state: ListState<T>TreeState<T>, ref: RefObject<FocusableElementnull> ): GridListItemAria useGridListSelectionCheckbox<T>( (props: AriaGridSelectionCheckboxProps, , state: ListState<T> )): GridSelectionCheckboxAria

Features#


A list can be built using <ul> or <ol> HTML elements, but does not support any user interactions. HTML lists are meant for static content, rather than lists with rich interactions like focusable elements within rows, keyboard navigation, row selection, etc. useGridList helps achieve accessible and interactive list components that can be styled as needed.

  • Item selection – Single or multiple selection, with optional checkboxes, disabled rows, and both toggle and replace selection behaviors.
  • Interactive children – List items may include interactive elements such as buttons, checkboxes, menus, etc.
  • Actions – Items support optional row actions such as navigation via click, tap, double click, or Enter key.
  • Async loading – Support for loading items asynchronously, with infinite and virtualized scrolling.
  • Keyboard navigation – List items and focusable children can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and selection modifier keys are supported as well.
  • Touch friendly – Selection and actions adapt their behavior depending on the device. For example, selection is activated via long press on touch when item actions are present.
  • Accessible – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent.

Note: Use useGridList when your list items may contain interactive elements such as buttons, checkboxes, menus, etc. within them. If your list items contain only static content such as text and images, then consider using useListBox instead for a slightly better screen reader experience (especially on mobile).

Anatomy#


List containerList itemList itemList itemList itemList item rowList item grid cellSelectioncheckbox(optional)

A grid list consists of a container element, with rows of data inside. The rows within a list may contain focusable elements or plain text content. If the list supports row selection, each row can optionally include a selection checkbox.

The useGridList and useGridListItem hooks handle keyboard, mouse, and other interactions to support row selection, in list navigation, and overall focus behavior. Those hooks handle exposing the list and its contents to assistive technology using ARIA. useGridListSelectionCheckbox handles row selection and associating each checkbox with its respective rows for assistive technology.

useGridList returns props that you should spread onto the list container element:

NameTypeDescription
gridPropsDOMAttributesProps for the grid element.

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

NameTypeDescription
rowPropsDOMAttributesProps for the list row element.
gridCellPropsDOMAttributesProps for the grid cell element within the list row.
descriptionPropsDOMAttributesProps for the list item description element, if any.
isPressedbooleanWhether the item is currently in a pressed state.
isSelectedbooleanWhether the item is currently selected.
isFocusedbooleanWhether the item is currently focused.
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.

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 where applicable.

Note that an aria-label or aria-labelledby must be passed to the list to identify the element to assistive technology.

State management#


useGridList requires knowledge of the rows in the list 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#


Lists are collection components that include rows as child elements. In this example, we'll use the standard HTML unordered list elements along with hooks from React Aria for each child. You may also use other elements like <div> to render these components as appropriate. We'll walk through creating the list container and list item, then add some additional behavior such as selection.

The useGridList hook will be used to render the outer most list element. It uses the useListState hook to construct the list's collection of rows, and manage state such as the focused row and row selection. We'll use the collection to iterate through the rows of the List and render the relevant components, which we'll define below.

You may notice the extra <div> with gridCellProps in our example. This is needed because we are following the ARIA grid pattern, which does not allow rows without any child gridcell elements.

import {mergeProps, useFocusRing, useGridList, useGridListItem} from 'react-aria';
import {useListState} from 'react-stately';
import {useRef} from 'react';

function List(props) {
  let state = useListState(props);
  let ref = useRef<HTMLUListElement | null>(null);
  let { gridProps } = useGridList(props, state, ref);

  return (
    <ul {...gridProps} ref={ref} className="list">
      {[...state.collection].map((item) => (
        <ListItem key={item.key} item={item} state={state} />
      ))}
    </ul>
  );
}

function ListItem({ item, state }) {
  let ref = React.useRef(null);
  let { rowProps, gridCellProps, isPressed } = useGridListItem(
    { node: item },
    state,
    ref
  );

  let { isFocusVisible, focusProps } = useFocusRing();
  let showCheckbox = state.selectionManager.selectionMode !== 'none' &&
    state.selectionManager.selectionBehavior === 'toggle';

  return (
    <li
      {...mergeProps(rowProps, focusProps)}
      ref={ref}
      className={`${isPressed ? 'pressed' : ''} ${
        isFocusVisible ? 'focus-visible' : ''
      }`}
    >
      <div {...gridCellProps}>
        {showCheckbox && <ListCheckbox item={item} state={state} />}
        {item.rendered}
      </div>
    </li>
  );
}
import {
  mergeProps,
  useFocusRing,
  useGridList,
  useGridListItem
} from 'react-aria';
import {useListState} from 'react-stately';
import {useRef} from 'react';

function List(props) {
  let state = useListState(props);
  let ref = useRef<HTMLUListElement | null>(null);
  let { gridProps } = useGridList(props, state, ref);

  return (
    <ul {...gridProps} ref={ref} className="list">
      {[...state.collection].map((item) => (
        <ListItem
          key={item.key}
          item={item}
          state={state}
        />
      ))}
    </ul>
  );
}

function ListItem({ item, state }) {
  let ref = React.useRef(null);
  let { rowProps, gridCellProps, isPressed } =
    useGridListItem(
      { node: item },
      state,
      ref
    );

  let { isFocusVisible, focusProps } = useFocusRing();
  let showCheckbox =
    state.selectionManager.selectionMode !== 'none' &&
    state.selectionManager.selectionBehavior === 'toggle';

  return (
    <li
      {...mergeProps(rowProps, focusProps)}
      ref={ref}
      className={`${isPressed ? 'pressed' : ''} ${
        isFocusVisible ? 'focus-visible' : ''
      }`}
    >
      <div {...gridCellProps}>
        {showCheckbox && (
          <ListCheckbox item={item} state={state} />
        )}
        {item.rendered}
      </div>
    </li>
  );
}
import {
  mergeProps,
  useFocusRing,
  useGridList,
  useGridListItem
} from 'react-aria';
import {useListState} from 'react-stately';
import {useRef} from 'react';

function List(props) {
  let state =
    useListState(props);
  let ref = useRef<
    | HTMLUListElement
    | null
  >(null);
  let { gridProps } =
    useGridList(
      props,
      state,
      ref
    );

  return (
    <ul
      {...gridProps}
      ref={ref}
      className="list"
    >
      {[
        ...state
          .collection
      ].map((item) => (
        <ListItem
          key={item.key}
          item={item}
          state={state}
        />
      ))}
    </ul>
  );
}

function ListItem(
  { item, state }
) {
  let ref = React.useRef(
    null
  );
  let {
    rowProps,
    gridCellProps,
    isPressed
  } = useGridListItem(
    { node: item },
    state,
    ref
  );

  let {
    isFocusVisible,
    focusProps
  } = useFocusRing();
  let showCheckbox =
    state
        .selectionManager
        .selectionMode !==
      'none' &&
    state
        .selectionManager
        .selectionBehavior ===
      'toggle';

  return (
    <li
      {...mergeProps(
        rowProps,
        focusProps
      )}
      ref={ref}
      className={`${
        isPressed
          ? 'pressed'
          : ''
      } ${
        isFocusVisible
          ? 'focus-visible'
          : ''
      }`}
    >
      <div
        {...gridCellProps}
      >
        {showCheckbox &&
          (
            <ListCheckbox
              item={item}
              state={state}
            />
          )}
        {item.rendered}
      </div>
    </li>
  );
}

Now we can render a basic example list, with multiple selection and interactive children in each item.

import {Item} from 'react-stately';

// Reuse the Button from your component library. See below.
import {Button} from 'your-component-library';

<List
  aria-label="Example List"
  selectionMode="multiple"
  selectionBehavior="replace"
>
  <Item textValue="Charizard">
    Charizard
    <Button onPress={() => alert(`Info for Charizard...`)}>Info</Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button onPress={() => alert(`Info for Blastoise...`)}>Info</Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button onPress={() => alert(`Info for Venusaur...`)}>Info</Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button onPress={() => alert(`Info for Pikachu...`)}>Info</Button>
  </Item>
</List>
import {Item} from 'react-stately';

// Reuse the Button from your component library. See below.
import {Button} from 'your-component-library';

<List
  aria-label="Example List"
  selectionMode="multiple"
  selectionBehavior="replace"
>
  <Item textValue="Charizard">
    Charizard
    <Button
      onPress={() => alert(`Info for Charizard...`)}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button
      onPress={() => alert(`Info for Blastoise...`)}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button onPress={() => alert(`Info for Venusaur...`)}>
      Info
    </Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button onPress={() => alert(`Info for Pikachu...`)}>
      Info
    </Button>
  </Item>
</List>
import {Item} from 'react-stately';

// Reuse the Button from your component library. See below.
import {Button} from 'your-component-library';

<List
  aria-label="Example List"
  selectionMode="multiple"
  selectionBehavior="replace"
>
  <Item textValue="Charizard">
    Charizard
    <Button
      onPress={() =>
        alert(
          `Info for Charizard...`
        )}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button
      onPress={() =>
        alert(
          `Info for Blastoise...`
        )}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button
      onPress={() =>
        alert(
          `Info for Venusaur...`
        )}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button
      onPress={() =>
        alert(
          `Info for Pikachu...`
        )}
    >
      Info
    </Button>
  </Item>
</List>
Show CSS
.list {
  padding: 0;
  list-style: none;
  background: var(--page-background);
  border: 1px solid var(--spectrum-global-color-gray-400);
  max-width: 400px;
  min-width: 200px;
  max-height: 250px;
  overflow: auto;
}

.list li {
  padding: 8px;
  outline: none;
  cursor: default;
}

.list li:nth-child(2n) {
  background: var(--spectrum-alias-highlight-hover);
}

.list li.pressed {
  background: var(--spectrum-global-color-gray-200);
}

.list li[aria-selected=true] {
  background: slateblue;
  color: white;
}

.list li.focus-visible {
  outline: 2px solid slateblue;
  outline-offset: -3px;
}

.list li.focus-visible[aria-selected=true] {
  outline-color: white;
}

.list li[aria-disabled] {
  opacity: 0.4;
}

.list [role=gridcell] {
  display: flex;
  align-items: center;
  gap: 4px;
}

.list li button {
  margin-left: auto;
}

/* iOS Safari has a bug that prevents accent-color: white from working. */
@supports not (-webkit-touch-callout: none) {
  .list li input[type=checkbox] {
    accent-color: white;
  }
}
.list {
  padding: 0;
  list-style: none;
  background: var(--page-background);
  border: 1px solid var(--spectrum-global-color-gray-400);
  max-width: 400px;
  min-width: 200px;
  max-height: 250px;
  overflow: auto;
}

.list li {
  padding: 8px;
  outline: none;
  cursor: default;
}

.list li:nth-child(2n) {
  background: var(--spectrum-alias-highlight-hover);
}

.list li.pressed {
  background: var(--spectrum-global-color-gray-200);
}

.list li[aria-selected=true] {
  background: slateblue;
  color: white;
}

.list li.focus-visible {
  outline: 2px solid slateblue;
  outline-offset: -3px;
}

.list li.focus-visible[aria-selected=true] {
  outline-color: white;
}

.list li[aria-disabled] {
  opacity: 0.4;
}

.list [role=gridcell] {
  display: flex;
  align-items: center;
  gap: 4px;
}

.list li button {
  margin-left: auto;
}

/* iOS Safari has a bug that prevents accent-color: white from working. */
@supports not (-webkit-touch-callout: none) {
  .list li input[type=checkbox] {
    accent-color: white;
  }
}
.list {
  padding: 0;
  list-style: none;
  background: var(--page-background);
  border: 1px solid var(--spectrum-global-color-gray-400);
  max-width: 400px;
  min-width: 200px;
  max-height: 250px;
  overflow: auto;
}

.list li {
  padding: 8px;
  outline: none;
  cursor: default;
}

.list li:nth-child(2n) {
  background: var(--spectrum-alias-highlight-hover);
}

.list li.pressed {
  background: var(--spectrum-global-color-gray-200);
}

.list li[aria-selected=true] {
  background: slateblue;
  color: white;
}

.list li.focus-visible {
  outline: 2px solid slateblue;
  outline-offset: -3px;
}

.list li.focus-visible[aria-selected=true] {
  outline-color: white;
}

.list li[aria-disabled] {
  opacity: 0.4;
}

.list [role=gridcell] {
  display: flex;
  align-items: center;
  gap: 4px;
}

.list li button {
  margin-left: auto;
}

/* iOS Safari has a bug that prevents accent-color: white from working. */
@supports not (-webkit-touch-callout: none) {
  .list li input[type=checkbox] {
    accent-color: white;
  }
}

Adding selection checkboxes#

Next, let's add support for selection checkboxes to allow the user to select items explicitly. This is done using the useGridListSelectionCheckbox hook. It is passed the key of the item it is contained within. When the user checks or unchecks the checkbox, the row will be added or removed from the List's selection.

The Checkbox component used in this example is independent and can be used separately from useGridList. The code is available below.

import {useGridListSelectionCheckbox} from 'react-aria';

// Reuse the Checkbox from your component library. See below for details.
import {Checkbox} from 'your-component-library';

function ListCheckbox({ item, state }) {
  let { checkboxProps } = useGridListSelectionCheckbox(
    { key: item.key },
    state
  );
  return <Checkbox {...checkboxProps} />;
}
import {useGridListSelectionCheckbox} from 'react-aria';

// Reuse the Checkbox from your component library. See below for details.
import {Checkbox} from 'your-component-library';

function ListCheckbox({ item, state }) {
  let { checkboxProps } = useGridListSelectionCheckbox({
    key: item.key
  }, state);
  return <Checkbox {...checkboxProps} />;
}
import {useGridListSelectionCheckbox} from 'react-aria';

// Reuse the Checkbox from your component library. See below for details.
import {Checkbox} from 'your-component-library';

function ListCheckbox(
  { item, state }
) {
  let { checkboxProps } =
    useGridListSelectionCheckbox(
      { key: item.key },
      state
    );
  return (
    <Checkbox
      {...checkboxProps}
    />
  );
}

The following example shows an example list with multiple selection using checkboxes and the default toggle selection behavior.

<List aria-label="List with selection" selectionMode="multiple">
  <Item textValue="Charizard">
    Charizard
    <Button onPress={() => alert(`Info for Charizard...`)}>Info</Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button onPress={() => alert(`Info for Blastoise...`)}>Info</Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button onPress={() => alert(`Info for Venusaur...`)}>Info</Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button onPress={() => alert(`Info for Pikachu...`)}>Info</Button>
  </Item>
</List>
<List
  aria-label="List with selection"
  selectionMode="multiple"
>
  <Item textValue="Charizard">
    Charizard
    <Button
      onPress={() => alert(`Info for Charizard...`)}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button
      onPress={() => alert(`Info for Blastoise...`)}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button onPress={() => alert(`Info for Venusaur...`)}>
      Info
    </Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button onPress={() => alert(`Info for Pikachu...`)}>
      Info
    </Button>
  </Item>
</List>
<List
  aria-label="List with selection"
  selectionMode="multiple"
>
  <Item textValue="Charizard">
    Charizard
    <Button
      onPress={() =>
        alert(
          `Info for Charizard...`
        )}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Blastoise">
    Blastoise
    <Button
      onPress={() =>
        alert(
          `Info for Blastoise...`
        )}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Venusaur">
    Venusaur
    <Button
      onPress={() =>
        alert(
          `Info for Venusaur...`
        )}
    >
      Info
    </Button>
  </Item>
  <Item textValue="Pikachu">
    Pikachu
    <Button
      onPress={() =>
        alert(
          `Info for Pikachu...`
        )}
    >
      Info
    </Button>
  </Item>
</List>

And that's it! We now have a fully interactive List component that can support keyboard navigation, single or multiple selection. In addition, it is fully accessible for screen readers and other assistive technology. See below for more examples of how to use the List component that we've built.

Checkbox#

The Checkbox component is used in the above example for row selection. It is built using the useCheckbox hook, and can be shared with many other components.

Show code
import {useToggleState} from 'react-stately';
import {useCheckbox} from 'react-aria';

function Checkbox(props) {
  let inputRef = useRef(null);
  let { inputProps } = useCheckbox(
    props,
    useToggleState(props),
    inputRef
  );
  return <input {...inputProps} ref={inputRef} />;
}
import {useToggleState} from 'react-stately';
import {useCheckbox} from 'react-aria';

function Checkbox(props) {
  let inputRef = useRef(null);
  let { inputProps } = useCheckbox(
    props,
    useToggleState(props),
    inputRef
  );
  return <input {...inputProps} ref={inputRef} />;
}
import {useToggleState} from 'react-stately';
import {useCheckbox} from 'react-aria';

function Checkbox(
  props
) {
  let inputRef = useRef(
    null
  );
  let { inputProps } =
    useCheckbox(
      props,
      useToggleState(
        props
      ),
      inputRef
    );
  return (
    <input
      {...inputProps}
      ref={inputRef}
    />
  );
}

Button#

The Button component is used in the above example to show how rows can contain interactive elements. It is built using the useButton hook, and can be shared with many other components.

Show code
import {useButton} from 'react-aria';

function Button(props) {
  let ref = React.useRef(null);
  let { buttonProps } = useButton(props, ref);
  return <button {...buttonProps} ref={ref}>{props.children}</button>;
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref = React.useRef(null);
  let { buttonProps } = useButton(props, ref);
  return (
    <button {...buttonProps} ref={ref}>
      {props.children}
    </button>
  );
}
import {useButton} from 'react-aria';

function Button(props) {
  let ref = React.useRef(
    null
  );
  let { buttonProps } =
    useButton(
      props,
      ref
    );
  return (
    <button
      {...buttonProps}
      ref={ref}
    >
      {props.children}
    </button>
  );
}

Usage#


Dynamic collections#

So far, our examples have shown static collections, where the data is hard coded. Dynamic collections, as shown below, can be used when the data comes from an external data source such as an API, or updates over time. In the example below, the rows are provided to the List via a render function.

function ExampleList(props) {
  let rows = [
    { id: 1, name: 'Games' },
    { id: 2, name: 'Program Files' },
    { id: 3, name: 'bootmgr' },
    { id: 4, name: 'log.txt' }
  ];

  return (
    <List
      aria-label="Example dynamic collection List"
      selectionMode="multiple"
      items={rows}
      {...props}
    >
      {(item) => (
        <Item textValue={item.name}>
          {item.name}
          <Button onPress={() => alert(`Info for ${item.name}...`)}>
            Info
          </Button>
        </Item>
      )}
    </List>
  );
}
function ExampleList(props) {
  let rows = [
    { id: 1, name: 'Games' },
    { id: 2, name: 'Program Files' },
    { id: 3, name: 'bootmgr' },
    { id: 4, name: 'log.txt' }
  ];

  return (
    <List
      aria-label="Example dynamic collection List"
      selectionMode="multiple"
      items={rows}
      {...props}
    >
      {(item) => (
        <Item textValue={item.name}>
          {item.name}
          <Button
            onPress={() =>
              alert(`Info for ${item.name}...`)}
          >
            Info
          </Button>
        </Item>
      )}
    </List>
  );
}
function ExampleList(
  props
) {
  let rows = [
    {
      id: 1,
      name: 'Games'
    },
    {
      id: 2,
      name:
        'Program Files'
    },
    {
      id: 3,
      name: 'bootmgr'
    },
    {
      id: 4,
      name: 'log.txt'
    }
  ];

  return (
    <List
      aria-label="Example dynamic collection List"
      selectionMode="multiple"
      items={rows}
      {...props}
    >
      {(item) => (
        <Item
          textValue={item
            .name}
        >
          {item.name}
          <Button
            onPress={() =>
              alert(
                `Info for ${item.name}...`
              )}
          >
            Info
          </Button>
        </Item>
      )}
    </List>
  );
}

Single selection#

By default, useListState doesn't allow row selection but this can be enabled using the selectionMode prop. Use defaultSelectedKeys to provide a default set of selected rows. Note that the value of the selected keys must match the key prop of the row.

The example below enables single selection mode, and uses defaultSelectedKeys to select the row with key equal to "2". A user can click on a different row to change the selection, or click on the same row again to deselect it entirely.

// Using the example above
<ExampleList
  aria-label="List with single selection"
  selectionMode="single"
  selectionBehavior="replace"
  defaultSelectedKeys={[2]}
/>
// Using the example above
<ExampleList
  aria-label="List with single selection"
  selectionMode="single"
  selectionBehavior="replace"
  defaultSelectedKeys={[2]}
/>
// Using the example above
<ExampleList
  aria-label="List with single selection"
  selectionMode="single"
  selectionBehavior="replace"
  defaultSelectedKeys={[
    2
  ]}
/>

Multiple selection#

Multiple selection can be enabled by setting selectionMode to multiple.

<ExampleList
  aria-label="List with multiple selection"
  selectionMode="multiple"
  defaultSelectedKeys={[2, 4]}
/>
<ExampleList
  aria-label="List with multiple selection"
  selectionMode="multiple"
  defaultSelectedKeys={[2, 4]}
/>
<ExampleList
  aria-label="List with multiple selection"
  selectionMode="multiple"
  defaultSelectedKeys={[
    2,
    4
  ]}
/>

Disallow empty selection#

useGridList also supports a disallowEmptySelection prop which forces the user to have at least one row in the List selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected.

<ExampleList
  aria-label="List with disallowed empty selection"
  selectionMode="multiple"
  defaultSelectedKeys={[2]}
  disallowEmptySelection
/>
<ExampleList
  aria-label="List with disallowed empty selection"
  selectionMode="multiple"
  defaultSelectedKeys={[2]}
  disallowEmptySelection
/>
<ExampleList
  aria-label="List with disallowed empty selection"
  selectionMode="multiple"
  defaultSelectedKeys={[
    2
  ]}
  disallowEmptySelection
/>

Controlled selection#

To programmatically control row selection, use the selectedKeys prop paired with the onSelectionChange callback. The key prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state accordingly.

function PokemonList(props) {
  let rows = [
    { id: 1, name: 'Charizard' },
    { id: 2, name: 'Blastoise' },
    { id: 3, name: 'Venusaur' },
    { id: 4, name: 'Pikachu' }
  ];

  let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));

  return (
    <List
      aria-label="List with controlled selection"
      items={rows}
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
      {...props}
    >
      {(item) => <Item>{item.name}</Item>}
    </List>
  );
}
function PokemonList(props) {
  let rows = [
    { id: 1, name: 'Charizard' },
    { id: 2, name: 'Blastoise' },
    { id: 3, name: 'Venusaur' },
    { id: 4, name: 'Pikachu' }
  ];

  let [selectedKeys, setSelectedKeys] = React.useState(
    new Set([2])
  );

  return (
    <List
      aria-label="List with controlled selection"
      items={rows}
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
      {...props}
    >
      {(item) => <Item>{item.name}</Item>}
    </List>
  );
}
function PokemonList(
  props
) {
  let rows = [
    {
      id: 1,
      name: 'Charizard'
    },
    {
      id: 2,
      name: 'Blastoise'
    },
    {
      id: 3,
      name: 'Venusaur'
    },
    {
      id: 4,
      name: 'Pikachu'
    }
  ];

  let [
    selectedKeys,
    setSelectedKeys
  ] = React.useState(
    new Set([2])
  );

  return (
    <List
      aria-label="List with controlled selection"
      items={rows}
      selectionMode="multiple"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
      {...props}
    >
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </List>
  );
}

Disabled rows#

You can disable specific rows by providing an array of keys to useListState via the disabledKeys prop. This will disable all interactions on disabled rows, unless the disabledBehavior prop is used to change this behavior. Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled.

// Using the example above
<PokemonList
  aria-label="List with disabled rows"
  selectionMode="multiple"
  disabledKeys={[3]}
/>
// Using the example above
<PokemonList
  aria-label="List with disabled rows"
  selectionMode="multiple"
  disabledKeys={[3]}
/>
// Using the example above
<PokemonList
  aria-label="List with disabled rows"
  selectionMode="multiple"
  disabledKeys={[3]}
/>

When disabledBehavior is set to selection, interactions such as focus, dragging, or actions can still be performed on disabled rows.

<PokemonList
  aria-label="List with selection disabled for disabled rows"
  selectionMode="multiple"
  disabledKeys={[3]}
  disabledBehavior="selection"
/>
<PokemonList
  aria-label="List with selection disabled for disabled rows"
  selectionMode="multiple"
  disabledKeys={[3]}
  disabledBehavior="selection"
/>
<PokemonList
  aria-label="List with selection disabled for disabled rows"
  selectionMode="multiple"
  disabledKeys={[3]}
  disabledBehavior="selection"
/>

Selection behavior#

By default, useGridList 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. The "toggle" selection mode is often paired with a checkbox in each row as an explicit affordance for 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 styles implement the behaviors defined in Aria Practices.

<PokemonList
  aria-label="List with replace selection behavior"
  selectionMode="multiple"
  selectionBehavior="replace"
/>
<PokemonList
  aria-label="List with replace selection behavior"
  selectionMode="multiple"
  selectionBehavior="replace"
/>
<PokemonList
  aria-label="List with replace selection behavior"
  selectionMode="multiple"
  selectionBehavior="replace"
/>

Row actions#

useGridList supports row actions via the onAction prop, which is useful for functionality such as navigation. When nothing is selected, the list performs actions by default when clicking or tapping a row. Items may be selected using the checkbox, or by long pressing on touch devices. When at least one item is selected, the list is in selection mode, and clicking or tapping a row toggles the selection. Actions may also be triggered via the Enter key, and selection using the Space key.

This behavior is slightly different when selectionBehavior="replace", where single clicking selects the row and actions are performed via double click. Touch and keyboard behaviors are unaffected.

<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}>
  <ExampleList
    aria-label="Checkbox selection list with row actions"
    selectionMode="multiple"
    selectionBehavior="toggle"
    onAction={(key) => alert(`Opening item ${key}...`)}
  />
  <ExampleList
    aria-label="Highlight selection list with row actions"
    selectionMode="multiple"
    selectionBehavior="replace"
    onAction={(key) => alert(`Opening item ${key}...`)}
  />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}>
  <ExampleList
    aria-label="Checkbox selection list with row actions"
    selectionMode="multiple"
    selectionBehavior="toggle"
    onAction={(key) => alert(`Opening item ${key}...`)}
  />
  <ExampleList
    aria-label="Highlight selection list with row actions"
    selectionMode="multiple"
    selectionBehavior="replace"
    onAction={(key) => alert(`Opening item ${key}...`)}
  />
</div>
<div
  style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: 24
  }}
>
  <ExampleList
    aria-label="Checkbox selection list with row actions"
    selectionMode="multiple"
    selectionBehavior="toggle"
    onAction={(key) =>
      alert(
        `Opening item ${key}...`
      )}
  />
  <ExampleList
    aria-label="Highlight selection list with row actions"
    selectionMode="multiple"
    selectionBehavior="replace"
    onAction={(key) =>
      alert(
        `Opening item ${key}...`
      )}
  />
</div>

Items in a GridList may also be links to another page or website. This can be achieved by passing the href prop to the <Item> component. Links behave the same way as described above for row actions depending on the selectionMode and selectionBehavior.

<List aria-label="Links" selectionMode="multiple">
  <Item href="https://adobe.com/" target="_blank">Adobe</Item>
  <Item href="https://apple.com/" target="_blank">Apple</Item>
  <Item href="https://google.com/" target="_blank">Google</Item>
  <Item href="https://microsoft.com/" target="_blank">Microsoft</Item>
</List>
<List aria-label="Links" selectionMode="multiple">
  <Item href="https://adobe.com/" target="_blank">
    Adobe
  </Item>
  <Item href="https://apple.com/" target="_blank">
    Apple
  </Item>
  <Item href="https://google.com/" target="_blank">
    Google
  </Item>
  <Item href="https://microsoft.com/" target="_blank">
    Microsoft
  </Item>
</List>
<List
  aria-label="Links"
  selectionMode="multiple"
>
  <Item
    href="https://adobe.com/"
    target="_blank"
  >
    Adobe
  </Item>
  <Item
    href="https://apple.com/"
    target="_blank"
  >
    Apple
  </Item>
  <Item
    href="https://google.com/"
    target="_blank"
  >
    Google
  </Item>
  <Item
    href="https://microsoft.com/"
    target="_blank"
  >
    Microsoft
  </Item>
</List>

Client side routing#

The <Item> component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the RouterProvider component at the root of your app. See the client side routing guide to learn how to set this up.

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';

function AsyncList() {
  let list = useAsyncList({
    async load({ signal, cursor }) {
      if (cursor) {
        cursor = cursor.replace(/^http:\/\//i, 'https://');
      }

      let res = await fetch(
        cursor || `https://swapi.py4e.com/api/people/?search=`,
        { signal }
      );
      let json = await res.json();

      return {
        items: json.results,
        cursor: json.next
      };
    }
  });

  return (
    <List
      selectionMode="multiple"
      aria-label="Async loading ListView example"
      items={list.items}
    >
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </List>
  );
}
import {useAsyncList} from 'react-stately';

function AsyncList() {
  let list = useAsyncList({
    async load({ signal, cursor }) {
      if (cursor) {
        cursor = cursor.replace(/^http:\/\//i, 'https://');
      }

      let res = await fetch(
        cursor ||
          `https://swapi.py4e.com/api/people/?search=`,
        { signal }
      );
      let json = await res.json();

      return {
        items: json.results,
        cursor: json.next
      };
    }
  });

  return (
    <List
      selectionMode="multiple"
      aria-label="Async loading ListView example"
      items={list.items}
    >
      {(item) => <Item key={item.name}>{item.name}</Item>}
    </List>
  );
}
import {useAsyncList} from 'react-stately';

function AsyncList() {
  let list =
    useAsyncList({
      async load(
        {
          signal,
          cursor
        }
      ) {
        if (cursor) {
          cursor = cursor
            .replace(
              /^http:\/\//i,
              'https://'
            );
        }

        let res =
          await fetch(
            cursor ||
              `https://swapi.py4e.com/api/people/?search=`,
            { signal }
          );
        let json =
          await res
            .json();

        return {
          items:
            json.results,
          cursor:
            json.next
        };
      }
    });

  return (
    <List
      selectionMode="multiple"
      aria-label="Async loading ListView example"
      items={list.items}
    >
      {(item) => (
        <Item
          key={item.name}
        >
          {item.name}
        </Item>
      )}
    </List>
  );
}

Internationalization#


useGridList handles some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching, and keyboard navigation is mirrored in right-to-left languages. You are responsible for localizing all text content within the List.

RTL#

In right-to-left languages, the list layout should be mirrored. The row contents should be ordered from right to left and the row's text alignment should be inverted. Ensure that your CSS accounts for this.