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.
install | yarn add @react-aria/listbox |
---|---|
version | 3.4.5 |
usage | import {useListBox, useOption, useListBoxSection} from '@react-aria/listbox' |
API#
useListBox<T>(
props: AriaListBoxOptions<T>,
state: ListState<T>,
ref: RefObject<HTMLElement>
): ListBoxAria
useOption<T>(
props: AriaOptionProps,
state: ListState<T>,
ref: RefObject<HTMLElement>
): 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#
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:
Name | Type | Description |
listBoxProps | HTMLAttributes<HTMLElement> | Props for the listbox element. |
labelProps | HTMLAttributes<HTMLElement> | Props 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:
Name | Type | Description |
optionProps | HTMLAttributes<HTMLElement> | Props for the option element. |
labelProps | HTMLAttributes<HTMLElement> | Props for the main text element inside the option. |
descriptionProps | HTMLAttributes<HTMLElement> | Props for the description text element inside the option, if any. |
isFocused | boolean | Whether the option is currently focused. |
isSelected | boolean | Whether the option is currently selected. |
isPressed | boolean | Whether the option is currently in a pressed state. |
isDisabled | boolean | Whether the option is disabled. |
useListBoxSection
returns props for a section:
Name | Type | Description |
itemProps | HTMLAttributes<HTMLElement> | Props for the wrapper list item. |
headingProps | HTMLAttributes<HTMLElement> | Props for the heading element, if any. |
groupProps | HTMLAttributes<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 {useListBox, useOption} from '@react-aria/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 { 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: 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 {useListBox, useOption} from '@react-aria/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 { 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: 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 {
useListBox,
useOption
} from '@react-aria/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 {
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: 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 {useListBoxSection} from '@react-aria/listbox';
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 {useListBoxSection} from '@react-aria/listbox';
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 {useListBoxSection} from '@react-aria/listbox';
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 { optionProps, labelProps, descriptionProps, isSelected, isDisabled } =
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="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 {
optionProps,
labelProps,
descriptionProps,
isSelected,
isDisabled
} = 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="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 {
optionProps,
labelProps,
descriptionProps,
isSelected,
isDisabled
} = 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="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.