ListBox
A listbox displays a list of options and allows a user to select one or more of them.
install | yarn add react-aria-components |
---|---|
version | 1.0.0-alpha.0 |
usage | import {ListBox} from 'react-aria-components' |
Example#
import {ListBox, Item} from 'react-aria-components';
<ListBox aria-label="Favorite animal" selectionMode="single">
<Item>Aardvark</Item>
<Item>Cat</Item>
<Item>Dog</Item>
<Item>Kangaroo</Item>
<Item>Panda</Item>
<Item>Snake</Item>
</ListBox>
import {Item, ListBox} from 'react-aria-components';
<ListBox
aria-label="Favorite animal"
selectionMode="single"
>
<Item>Aardvark</Item>
<Item>Cat</Item>
<Item>Dog</Item>
<Item>Kangaroo</Item>
<Item>Panda</Item>
<Item>Snake</Item>
</ListBox>
import {
Item,
ListBox
} from 'react-aria-components';
<ListBox
aria-label="Favorite animal"
selectionMode="single"
>
<Item>Aardvark</Item>
<Item>Cat</Item>
<Item>Dog</Item>
<Item>Kangaroo</Item>
<Item>Panda</Item>
<Item>Snake</Item>
</ListBox>
Show CSS
.react-aria-ListBox {
--highlight-background: slateblue;
--highlight-foreground: white;
--border-color: var(--spectrum-global-color-gray-400);
--background-color: var(--page-background);
--text-color: var(--spectrum-alias-text-color);
--text-color-disabled: var(--spectrum-alias-text-color-disabled);
display: flex;
flex-direction: column;
max-height: inherit;
overflow: auto;
padding: 2px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--background-color);
outline: none;
width: 250px;
max-height: 300px;
min-height: 100px;
box-sizing: border-box;
&[data-empty] {
align-items: center;
justify-content: center;
font-style: italic;
}
&[data-focus-visible] {
border-color: var(--highlight-background);
box-shadow: 0 0 0 1px var(--highlight-background);
}
.react-aria-Section:not(:first-child) {
margin-top: 12px;
}
.react-aria-Header {
font-size: 1.143rem;
font-weight: bold;
padding: 0 0.714rem;
}
.react-aria-Item {
margin: 2px;
padding: 0.286rem 0.571rem;
border-radius: 6px;
outline: none;
cursor: default;
color: var(--text-color);
font-size: 1.072rem;
position: relative;
display: flex;
flex-direction: column;
&[data-focus-visible] {
box-shadow: inset 0 0 0 2px var(--highlight-background);
}
&[aria-selected=true] {
background: var(--highlight-background);
color: var(--highlight-foreground);
&[data-focus-visible] {
box-shadow: inset 0 0 0 2px var(--highlight-background), inset 0 0 0 4px var(--highlight-foreground);
}
}
&[aria-disabled] {
color: var(--text-color-disabled);
}
[slot=label] {
font-weight: bold;
}
[slot=description] {
font-size: small;
}
}
}
@media (forced-colors: active) {
.react-aria-ListBox {
forced-color-adjust: none;
--highlight-background: Highlight;
--highlight-foreground: HighlightText;
--border-color: ButtonBorder;
--background-color: ButtonFace;
--separator-color: ButtonBorder;
--text-color: ButtonText;
--text-color-disabled: GrayText;
}
}
.react-aria-ListBox {
--highlight-background: slateblue;
--highlight-foreground: white;
--border-color: var(--spectrum-global-color-gray-400);
--background-color: var(--page-background);
--text-color: var(--spectrum-alias-text-color);
--text-color-disabled: var(--spectrum-alias-text-color-disabled);
display: flex;
flex-direction: column;
max-height: inherit;
overflow: auto;
padding: 2px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--background-color);
outline: none;
width: 250px;
max-height: 300px;
min-height: 100px;
box-sizing: border-box;
&[data-empty] {
align-items: center;
justify-content: center;
font-style: italic;
}
&[data-focus-visible] {
border-color: var(--highlight-background);
box-shadow: 0 0 0 1px var(--highlight-background);
}
.react-aria-Section:not(:first-child) {
margin-top: 12px;
}
.react-aria-Header {
font-size: 1.143rem;
font-weight: bold;
padding: 0 0.714rem;
}
.react-aria-Item {
margin: 2px;
padding: 0.286rem 0.571rem;
border-radius: 6px;
outline: none;
cursor: default;
color: var(--text-color);
font-size: 1.072rem;
position: relative;
display: flex;
flex-direction: column;
&[data-focus-visible] {
box-shadow: inset 0 0 0 2px var(--highlight-background);
}
&[aria-selected=true] {
background: var(--highlight-background);
color: var(--highlight-foreground);
&[data-focus-visible] {
box-shadow: inset 0 0 0 2px var(--highlight-background), inset 0 0 0 4px var(--highlight-foreground);
}
}
&[aria-disabled] {
color: var(--text-color-disabled);
}
[slot=label] {
font-weight: bold;
}
[slot=description] {
font-size: small;
}
}
}
@media (forced-colors: active) {
.react-aria-ListBox {
forced-color-adjust: none;
--highlight-background: Highlight;
--highlight-foreground: HighlightText;
--border-color: ButtonBorder;
--background-color: ButtonFace;
--separator-color: ButtonBorder;
--text-color: ButtonText;
--text-color-disabled: GrayText;
}
}
.react-aria-ListBox {
--highlight-background: slateblue;
--highlight-foreground: white;
--border-color: var(--spectrum-global-color-gray-400);
--background-color: var(--page-background);
--text-color: var(--spectrum-alias-text-color);
--text-color-disabled: var(--spectrum-alias-text-color-disabled);
display: flex;
flex-direction: column;
max-height: inherit;
overflow: auto;
padding: 2px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--background-color);
outline: none;
width: 250px;
max-height: 300px;
min-height: 100px;
box-sizing: border-box;
&[data-empty] {
align-items: center;
justify-content: center;
font-style: italic;
}
&[data-focus-visible] {
border-color: var(--highlight-background);
box-shadow: 0 0 0 1px var(--highlight-background);
}
.react-aria-Section:not(:first-child) {
margin-top: 12px;
}
.react-aria-Header {
font-size: 1.143rem;
font-weight: bold;
padding: 0 0.714rem;
}
.react-aria-Item {
margin: 2px;
padding: 0.286rem 0.571rem;
border-radius: 6px;
outline: none;
cursor: default;
color: var(--text-color);
font-size: 1.072rem;
position: relative;
display: flex;
flex-direction: column;
&[data-focus-visible] {
box-shadow: inset 0 0 0 2px var(--highlight-background);
}
&[aria-selected=true] {
background: var(--highlight-background);
color: var(--highlight-foreground);
&[data-focus-visible] {
box-shadow: inset 0 0 0 2px var(--highlight-background), inset 0 0 0 4px var(--highlight-foreground);
}
}
&[aria-disabled] {
color: var(--text-color-disabled);
}
[slot=label] {
font-weight: bold;
}
[slot=description] {
font-size: small;
}
}
}
@media (forced-colors: active) {
.react-aria-ListBox {
forced-color-adjust: none;
--highlight-background: Highlight;
--highlight-foreground: HighlightText;
--border-color: ButtonBorder;
--background-color: ButtonFace;
--separator-color: ButtonBorder;
--text-color: ButtonText;
--text-color-disabled: GrayText;
}
}
Features#
A listbox can be built using the <select>
and <option> HTML elements, but this is
not possible to style consistently cross browser. ListBox
helps you build accessible
listbox components that can be styled as needed.
- Item selection – Single or multiple selection, disabled rows, and both
toggle
andreplace
selection behaviors. - Keyboard navigation – List items 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 behavior adapts depending on the device. For example, selection occurs on mouse down but on touch up, which is consistent with native conventions.
- Accessible – Follows the ARIA listbox pattern, with support for items and sections, and slots for label and description elements within each item for improved screen reader announcement.
- Styleable – Items include builtin states for styling, such as hover, press, focus, selected, and disabled.
Note: ListBox
only handles the list itself. For a dropdown, see Select.
Anatomy#
A listbox consists of a container element, with a list of options or groups inside. Users can select one or more options by clicking, tapping, or navigating with the keyboard.
Concepts#
ListBox
makes use of the following concepts:
Props#
ListBox#
Name | Type | Default | Description |
selectionBehavior | SelectionBehavior | — | How multiple selection should behave in the collection. |
dragAndDropHooks | DragAndDropHooks | — | The drag and drop hooks returned by useDragAndDrop used to enable drag and drop behavior for the ListBox. |
renderEmptyState | () => ReactNode | — | Provides content to display when there are no items in the list. |
label | ReactNode | — | An optional visual label for the listbox. |
autoFocus | boolean | FocusStrategy | — | Whether to auto focus the listbox or an option. |
shouldFocusWrap | boolean | — | Whether focus should wrap around when the end/start is reached. |
items | Iterable<T> | — | Item objects in the collection. |
disabledKeys | Iterable<Key> | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. |
selectionMode | SelectionMode | — | The type of selection that is allowed in the collection. |
disallowEmptySelection | boolean | — | Whether the collection allows empty selection. |
selectedKeys | 'all' | Iterable<Key> | — | The currently selected keys in the collection (controlled). |
defaultSelectedKeys | 'all' | Iterable<Key> | — | The initial selected keys in the collection (uncontrolled). |
children | ReactNode | (
(item: T
)) => ReactElement | — | The contents of the collection. |
className | string | (
(values: ListBoxRenderProps
)) => string | — | The CSS className for the element. A function may be provided to compute the class based on component state. |
style | CSSProperties | (
(values: ListBoxRenderProps
)) => CSSProperties | — | The inline style for the element. A function may be provided to compute the style based on component state. |
Events
Name | Type | Default | Description |
onAction | (
(key: Key
)) => void | — | Handler that is called when a user performs an action on an item. The exact user event depends on
the collection's |
onSelectionChange | (
(keys: Selection
)) => any | — | Handler that is called when the selection changes. |
onFocus | (
(e: FocusEvent<Target>
)) => void | — | Handler that is called when the element receives focus. |
onBlur | (
(e: FocusEvent<Target>
)) => void | — | Handler that is called when the element loses focus. |
onFocusChange | (
(isFocused: boolean
)) => void | — | Handler that is called when the element's focus status changes. |
Layout
Name | Type | Default | Description |
slot | string | — | A slot name for the component. Slots allow the component to receive props from a parent component. |
Accessibility
Name | Type | Default | Description |
id | string | — | The element's unique identifier. See MDN. |
aria-label | string | — | Defines a string value that labels the current element. |
aria-labelledby | string | — | Identifies the element (or elements) that labels the current element. |
aria-describedby | string | — | Identifies the element (or elements) that describes the object. |
aria-details | string | — | Identifies the element (or elements) that provide a detailed, extended description for the object. |
Section#
A <Section>
defines the child items for a section within a <ListBox>
. It may also contain an optional <Header>
element. If there is no header, then an aria-label
must be provided to identify the section to assistive technologies.
Show props
Name | Type | Default | Description |
value | object | — | The object value that this section represents. When using dynamic collections, this is set automatically. |
children | ReactNode | (
(item: object
)) => ReactElement | — | Static child items or a function to render children. |
items | Iterable<T> | — | Item objects in the section. |
className | string | — | The CSS className for the element. |
style | CSSProperties | — | The inline style for the element. |
Header#
A <Header>
defines the title for a <Section>
. It accepts all DOM attributes.
Item#
An <Item>
defines a single option within a <ListBox>
. If the children
are not plain text, then the textValue
prop must also be set to a plain text representation, which will be used for typeahead in the ListBox.
Show props
Name | Type | Default | Description |
value | object | — | The object value that this item represents. When using dynamic collections, this is set automatically. |
title | ReactNode | — | Rendered contents of the item if children contains child items. |
textValue | string | — | A string representation of the item's contents, used for features like typeahead. |
childItems | Iterable<T> | — | A list of child item objects. Used for dynamic collections. |
hasChildItems | boolean | — | Whether this item has children, even if not loaded yet. |
children | ReactNode | (
(values: ItemRenderProps
)) => ReactNode | — | The children of the component. A function may be provided to alter the children based on component state. |
className | string | (
(values: ItemRenderProps
)) => string | — | The CSS className for the element. A function may be provided to compute the class based on component state. |
style | CSSProperties | (
(values: ItemRenderProps
)) => CSSProperties | — | The inline style for the element. A function may be provided to compute the style based on component state. |
Styling#
React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className
attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName
naming convention.
.react-aria-ListBox {
/* ... */
}
.react-aria-ListBox {
/* ... */
}
.react-aria-ListBox {
/* ... */
}
A custom className
can also be specified on any component. This overrides the default className
provided by React Aria with your own.
<ListBox className="my-listbox">
{/* ... */}
</ListBox>
<ListBox className="my-listbox">
{/* ... */}
</ListBox>
<ListBox className="my-listbox">
{/* ... */}
</ListBox>
In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using DOM attributes, which you can target in CSS selectors. These are ARIA attributes wherever possible, or data attributes when a relevant ARIA attribute does not exist. For example:
.react-aria-Item[aria-selected=true] {
/* ... */
}
.react-aria-Item[data-focused] {
/* ... */
}
.react-aria-Item[aria-selected=true] {
/* ... */
}
.react-aria-Item[data-focused] {
/* ... */
}
.react-aria-Item[aria-selected=true] {
/* ... */
}
.react-aria-Item[data-focused] {
/* ... */
}
The className
and style
props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.
<Item
className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
Item
</Item>
<Item
className={({ isSelected }) =>
isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
Item
</Item>
<Item
className={(
{ isSelected }
) =>
isSelected
? 'bg-blue-400'
: 'bg-gray-100'}
>
Item
</Item>
Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render a checkmark icon when an item is selected.
<Item>
{({isSelected}) => (
<>
{isSelected && <CheckmarkIcon />}
Item
</>
)}
</Item>
<Item>
{({isSelected}) => (
<>
{isSelected && <CheckmarkIcon />}
Item
</>
)}
</Item>
<Item>
{(
{ isSelected }
) => (
<>
{isSelected && (
<CheckmarkIcon />
)}
Item
</>
)}
</Item>
The states and selectors for each component used in a ListBox
are documented below.
ListBox#
A ListBox
can be targeted with the .react-aria-ListBox
CSS selector, or by overriding with a custom className
. It supports the following states and render props:
Name | CSS Selector | Description |
isEmpty | [data-empty] | Whether the listbox has no items and should display its empty state. |
isFocused | [data-focused] | Whether the listbox is currently focused. |
isFocusVisible | [data-focus-visible] | Whether the listbox is currently keyboard focused. |
isDropTarget | [data-drop-target] | Whether the listbox is currently the active drop target. |
Section#
A Section
can be targeted with the .react-aria-Section
CSS selector, or by overriding with a custom className
. See sections for examples.
Header#
A Header
within a Section
can be targeted with the .react-aria-Header
CSS selector, or by overriding with a custom className
. See sections for examples.
Item#
An Item
can be targeted with the .react-aria-Item
CSS selector, or by overriding with a custom className
. It supports the following states and render props:
Name | CSS Selector | Description |
isHovered | [data-hovered] | Whether the item is currently hovered with a mouse. |
isPressed | [data-pressed] | Whether the item is currently in a pressed state. |
isSelected | [aria-selected=true] | Whether the item is currently selected. |
isFocused | [data-focused] | Whether the item is currently focused. |
isFocusVisible | [data-focus-visible] | Whether the item is currently keyboard focused. |
isDisabled | [aria-disabled] | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may
not be focused. Dependent on |
selectionMode | — | The type of selection that is allowed in the collection. |
selectionBehavior | — | The selection behavior for the collection. |
allowsDragging | [draggable] | Whether the item allows dragging. |
isDragging | [data-dragging] | Whether the item is currently being dragged. |
isDropTarget | [data-drop-target] | Whether the item is currently an active drop target. |
Items also support two slots: a label, and a description. When provided using the <Text>
element, the item will have aria-labelledby
and aria-describedby
attributes pointing to these slots, improving screen reader announcement. See complex items for an example.
Note that items may not contain interactive children such as buttons, as screen readers will not be able to access them.
Reusable wrappers#
If you will use a ListBox in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency.
This example wraps ListBox
and all of its children together into a single component which accepts a label
prop and children
, which are passed through to the right places. The Item
component is also wrapped to apply class names based on the current state, as described above.
import type {ItemProps, ListBoxProps} from 'react-aria-components';
function MyListBox<T extends object>({ children, ...props }: ListBoxProps<T>) {
return (
<ListBox {...props} className="my-listbox">
{children}
</ListBox>
);
}
function MyItem(props: ItemProps) {
return (
<Item
{...props}
className={({ isFocusVisible, isSelected }) =>
`my-item `}
/>
);
}
<MyListBox aria-label="Ice cream flavor" selectionMode="single">
<MyItem>Chocolate</MyItem>
<MyItem>Mint</MyItem>
<MyItem>Strawberry</MyItem>
<MyItem>Vanilla</MyItem>
</MyListBox>
import type {
ItemProps,
ListBoxProps
} from 'react-aria-components';
function MyListBox<T extends object>(
{ children, ...props }: ListBoxProps<T>
) {
return (
<ListBox {...props} className="my-listbox">
{children}
</ListBox>
);
}
function MyItem(props: ItemProps) {
return (
<Item
{...props}
className={({ isFocusVisible, isSelected }) =>
`my-item `}
/>
);
}
<MyListBox
aria-label="Ice cream flavor"
selectionMode="single"
>
<MyItem>Chocolate</MyItem>
<MyItem>Mint</MyItem>
<MyItem>Strawberry</MyItem>
<MyItem>Vanilla</MyItem>
</MyListBox>
import type {
ItemProps,
ListBoxProps
} from 'react-aria-components';
function MyListBox<
T extends object
>(
{ children, ...props }:
ListBoxProps<T>
) {
return (
<ListBox
{...props}
className="my-listbox"
>
{children}
</ListBox>
);
}
function MyItem(
props: ItemProps
) {
return (
<Item
{...props}
className={(
{
isFocusVisible,
isSelected
}
) =>
`my-item `}
/>
);
}
<MyListBox
aria-label="Ice cream flavor"
selectionMode="single"
>
<MyItem>
Chocolate
</MyItem>
<MyItem>Mint</MyItem>
<MyItem>
Strawberry
</MyItem>
<MyItem>
Vanilla
</MyItem>
</MyListBox>
Show CSS
.my-listbox {
max-height: inherit;
overflow: auto;
padding: 2px;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 6px;
background: var(--page-background);
outline: none;
max-width: 250px;
max-height: 300px;
box-sizing: border-box;
}
.my-item {
--highlight: #e70073;
margin: 2px;
padding: 0.286rem 0.571rem;
border-radius: 6px;
outline: none;
cursor: default;
color: var(--spectrum-global-color-gray-800);
font-size: 1.072rem;
&.selected {
background: var(--highlight);
color: white;
}
&.focused {
box-shadow: 0 0 0 2px var(--page-background), 0 0 0 4px var(--highlight);
}
}
@media (forced-colors: active) {
.my-listbox {
forced-color-adjust: none;
border-color: ButtonBorder;
background: Canvas;
}
.my-item {
color: ButtonText;
--highlight: Highlight;
&.selected {
color: HighlightText;
}
}
}
.my-listbox {
max-height: inherit;
overflow: auto;
padding: 2px;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 6px;
background: var(--page-background);
outline: none;
max-width: 250px;
max-height: 300px;
box-sizing: border-box;
}
.my-item {
--highlight: #e70073;
margin: 2px;
padding: 0.286rem 0.571rem;
border-radius: 6px;
outline: none;
cursor: default;
color: var(--spectrum-global-color-gray-800);
font-size: 1.072rem;
&.selected {
background: var(--highlight);
color: white;
}
&.focused {
box-shadow: 0 0 0 2px var(--page-background), 0 0 0 4px var(--highlight);
}
}
@media (forced-colors: active) {
.my-listbox {
forced-color-adjust: none;
border-color: ButtonBorder;
background: Canvas;
}
.my-item {
color: ButtonText;
--highlight: Highlight;
&.selected {
color: HighlightText;
}
}
}
.my-listbox {
max-height: inherit;
overflow: auto;
padding: 2px;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 6px;
background: var(--page-background);
outline: none;
max-width: 250px;
max-height: 300px;
box-sizing: border-box;
}
.my-item {
--highlight: #e70073;
margin: 2px;
padding: 0.286rem 0.571rem;
border-radius: 6px;
outline: none;
cursor: default;
color: var(--spectrum-global-color-gray-800);
font-size: 1.072rem;
&.selected {
background: var(--highlight);
color: white;
}
&.focused {
box-shadow: 0 0 0 2px var(--page-background), 0 0 0 4px var(--highlight);
}
}
@media (forced-colors: active) {
.my-listbox {
forced-color-adjust: none;
border-color: ButtonBorder;
background: Canvas;
}
.my-item {
color: ButtonText;
--highlight: Highlight;
&.selected {
color: HighlightText;
}
}
}
Usage#
Dynamic collections#
ListBox
follows the Collection Components API, accepting both static and dynamic collections.
The examples above show 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 an id
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 an id
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 aria-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
aria-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
aria-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 id
prop of the items.
See the react-stately
Selection docs for more details.
import type {Selection} from 'react-aria-components';
function Example() {
let [selected, setSelected] = React.useState<Selection>(new Set(['cheese']));
return (
<>
<ListBox
aria-label="Sandwich contents"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="cheese">Cheese</Item>
<Item id="tuna">Tuna Salad</Item>
<Item id="egg">Egg Salad</Item>
<Item id="ham">Ham</Item>
</ListBox>
<p>
Current selection (controlled):{' '}
{selected === 'all' ? 'all' : [...selected].join(', ')}
</p>
</>
);
}
import type {Selection} from 'react-aria-components';
function Example() {
let [selected, setSelected] = React.useState<Selection>(
new Set(['cheese'])
);
return (
<>
<ListBox
aria-label="Sandwich contents"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="cheese">Cheese</Item>
<Item id="tuna">Tuna Salad</Item>
<Item id="egg">Egg Salad</Item>
<Item id="ham">Ham</Item>
</ListBox>
<p>
Current selection (controlled): {selected === 'all'
? 'all'
: [...selected].join(', ')}
</p>
</>
);
}
import type {Selection} from 'react-aria-components';
function Example() {
let [
selected,
setSelected
] = React.useState<
Selection
>(new Set(['cheese']));
return (
<>
<ListBox
aria-label="Sandwich contents"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
>
<Item id="lettuce">
Lettuce
</Item>
<Item id="tomato">
Tomato
</Item>
<Item id="cheese">
Cheese
</Item>
<Item id="tuna">
Tuna Salad
</Item>
<Item id="egg">
Egg Salad
</Item>
<Item id="ham">
Ham
</Item>
</ListBox>
<p>
Current selection
(controlled):
{' '}
{selected ===
'all'
? 'all'
: [...selected]
.join(', ')}
</p>
</>
);
}
Selection behavior#
By default, ListBox
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
aria-label="Sandwich contents"
selectionMode="multiple"
selectionBehavior="replace"
>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="cheese">Cheese</Item>
<Item id="tuna">Tuna Salad</Item>
<Item id="egg">Egg Salad</Item>
<Item id="ham">Ham</Item>
</ListBox>
<ListBox
aria-label="Sandwich contents"
selectionMode="multiple"
selectionBehavior="replace"
>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="cheese">Cheese</Item>
<Item id="tuna">Tuna Salad</Item>
<Item id="egg">Egg Salad</Item>
<Item id="ham">Ham</Item>
</ListBox>
<ListBox
aria-label="Sandwich contents"
selectionMode="multiple"
selectionBehavior="replace"
>
<Item id="lettuce">
Lettuce
</Item>
<Item id="tomato">
Tomato
</Item>
<Item id="cheese">
Cheese
</Item>
<Item id="tuna">
Tuna Salad
</Item>
<Item id="egg">
Egg Salad
</Item>
<Item id="ham">
Ham
</Item>
</ListBox>
Sections#
ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a Section
element. A <Header>
element may also be included to label the section.
Static items#
import {Section, Header} from 'react-aria-components';
<ListBox aria-label="Sandwich contents" selectionMode="multiple">
<Section>
<Header>Veggies</Header>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="onion">Onion</Item>
</Section>
<Section>
<Header>Protein</Header>
<Item id="ham">Ham</Item>
<Item id="tuna">Tuna</Item>
<Item id="tofu">Tofu</Item>
</Section>
<Section>
<Header>Condiments</Header>
<Item id="mayo">Mayonaise</Item>
<Item id="mustard">Mustard</Item>
<Item id="ranch">Ranch</Item>
</Section>
</ListBox>
import {Header, Section} from 'react-aria-components';
<ListBox
aria-label="Sandwich contents"
selectionMode="multiple"
>
<Section>
<Header>Veggies</Header>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="onion">Onion</Item>
</Section>
<Section>
<Header>Protein</Header>
<Item id="ham">Ham</Item>
<Item id="tuna">Tuna</Item>
<Item id="tofu">Tofu</Item>
</Section>
<Section>
<Header>Condiments</Header>
<Item id="mayo">Mayonaise</Item>
<Item id="mustard">Mustard</Item>
<Item id="ranch">Ranch</Item>
</Section>
</ListBox>
import {
Header,
Section
} from 'react-aria-components';
<ListBox
aria-label="Sandwich contents"
selectionMode="multiple"
>
<Section>
<Header>
Veggies
</Header>
<Item id="lettuce">
Lettuce
</Item>
<Item id="tomato">
Tomato
</Item>
<Item id="onion">
Onion
</Item>
</Section>
<Section>
<Header>
Protein
</Header>
<Item id="ham">
Ham
</Item>
<Item id="tuna">
Tuna
</Item>
<Item id="tofu">
Tofu
</Item>
</Section>
<Section>
<Header>
Condiments
</Header>
<Item id="mayo">
Mayonaise
</Item>
<Item id="mustard">
Mustard
</Item>
<Item id="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. If the section also has a header,
the Collection
component can be used to render the child items.
import type {Selection} from 'react-aria-components';
import {Collection} from 'react-aria-components';
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
aria-label="Pick an animal"
items={options}
selectedKeys={selected}
selectionMode="single"
onSelectionChange={setSelected}>
{section => (
<Section id={section.name}>
<Header>{section.name}</Header>
<Collection items={section.children}>
{item => <Item>{item.name}</Item>}
</Collection>
</Section>
)}
</ListBox>
);
}
import type {Selection} from 'react-aria-components';
import {Collection} from 'react-aria-components';
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
aria-label="Pick an animal"
items={options}
selectedKeys={selected}
selectionMode="single"
onSelectionChange={setSelected}
>
{(section) => (
<Section id={section.name}>
<Header>{section.name}</Header>
<Collection items={section.children}>
{(item) => <Item>{item.name}</Item>}
</Collection>
</Section>
)}
</ListBox>
);
}
import type {Selection} from 'react-aria-components';
import {Collection} from 'react-aria-components';
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
aria-label="Pick an animal"
items={options}
selectedKeys={selected}
selectionMode="single"
onSelectionChange={setSelected}
>
{(section) => (
<Section
id={section
.name}
>
<Header>
{section
.name}
</Header>
<Collection
items={section
.children}
>
{(item) => (
<Item>
{item
.name}
</Item>
)}
</Collection>
</Section>
)}
</ListBox>
);
}
Accessibility#
Sections without a title
must provide an aria-label
for accessibility.
Complex options#
By default, options items that only contain text will be labeled by the contents of the item. The <Text>
component may be used to add a description using the slot="description"
prop. Slots improve screen reader announcements, and may also be used for styling and layout purposes.
NOTE: listbox options cannot contain interactive content (e.g. buttons, checkboxes, etc.). For these cases, see GridList instead.
import {Text} from 'react-aria-components';
<ListBox aria-label="Permissions" selectionMode="single">
<Item textValue="Read">
<Text slot="label">Read</Text>
<Text slot="description">Read only</Text>
</Item>
<Item textValue="Write">
<Text slot="label">Write</Text>
<Text slot="description">Read and write only</Text>
</Item>
<Item textValue="Admin">
<Text slot="label">Admin</Text>
<Text slot="description">Full access</Text>
</Item>
</ListBox>
import {Text} from 'react-aria-components';
<ListBox aria-label="Permissions" selectionMode="single">
<Item textValue="Read">
<Text slot="label">Read</Text>
<Text slot="description">Read only</Text>
</Item>
<Item textValue="Write">
<Text slot="label">Write</Text>
<Text slot="description">Read and write only</Text>
</Item>
<Item textValue="Admin">
<Text slot="label">Admin</Text>
<Text slot="description">Full access</Text>
</Item>
</ListBox>
import {Text} from 'react-aria-components';
<ListBox
aria-label="Permissions"
selectionMode="single"
>
<Item textValue="Read">
<Text slot="label">
Read
</Text>
<Text slot="description">
Read only
</Text>
</Item>
<Item textValue="Write">
<Text slot="label">
Write
</Text>
<Text slot="description">
Read and write
only
</Text>
</Item>
<Item textValue="Admin">
<Text slot="label">
Admin
</Text>
<Text slot="description">
Full access
</Text>
</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/data';
interface Character {
name: string;
}
function AsyncLoadingExample() {
let list = useAsyncList<Character>({
async load({ signal, filterText }) {
let res = await fetch(
`https://pokeapi.co/api/v2/pokemon`,
{ signal }
);
let json = await res.json();
return {
items: json.results
};
}
});
return (
<ListBox
aria-label="Pick a Pokemon"
items={list.items}
selectionMode="single"
>
{(item) => <Item id={item.name}>{item.name}</Item>}
</ListBox>
);
}
import {useAsyncList} from '@react-stately/data';
interface Character {
name: string;
}
function AsyncLoadingExample() {
let list = useAsyncList<Character>({
async load({ signal, filterText }) {
let res = await fetch(
`https://pokeapi.co/api/v2/pokemon`,
{ signal }
);
let json = await res.json();
return {
items: json.results
};
}
});
return (
<ListBox
aria-label="Pick a Pokemon"
items={list.items}
selectionMode="single"
>
{(item) => <Item id={item.name}>{item.name}</Item>}
</ListBox>
);
}
import {useAsyncList} from '@react-stately/data';
interface Character {
name: string;
}
function AsyncLoadingExample() {
let list =
useAsyncList<
Character
>({
async load(
{
signal,
filterText
}
) {
let res =
await fetch(
`https://pokeapi.co/api/v2/pokemon`,
{ signal }
);
let json =
await res
.json();
return {
items:
json.results
};
}
});
return (
<ListBox
aria-label="Pick a Pokemon"
items={list.items}
selectionMode="single"
>
{(item) => (
<Item
id={item.name}
>
{item.name}
</Item>
)}
</ListBox>
);
}
Disabled items#
ListBox
supports marking items as disabled using the disabledKeys
prop. Each key in this list
corresponds with the id
prop passed to the Item
component, or automatically derived from the values passed
to the items
prop. See Collections for more details.
<ListBox
aria-label="Choose sandwich contents"
selectionMode="multiple"
disabledKeys={['tuna']}
>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="cheese">Cheese</Item>
<Item id="tuna">Tuna Salad</Item>
<Item id="egg">Egg Salad</Item>
<Item id="ham">Ham</Item>
</ListBox>
<ListBox
aria-label="Choose sandwich contents"
selectionMode="multiple"
disabledKeys={['tuna']}
>
<Item id="lettuce">Lettuce</Item>
<Item id="tomato">Tomato</Item>
<Item id="cheese">Cheese</Item>
<Item id="tuna">Tuna Salad</Item>
<Item id="egg">Egg Salad</Item>
<Item id="ham">Ham</Item>
</ListBox>
<ListBox
aria-label="Choose sandwich contents"
selectionMode="multiple"
disabledKeys={[
'tuna'
]}
>
<Item id="lettuce">
Lettuce
</Item>
<Item id="tomato">
Tomato
</Item>
<Item id="cheese">
Cheese
</Item>
<Item id="tuna">
Tuna Salad
</Item>
<Item id="egg">
Egg Salad
</Item>
<Item id="ham">
Ham
</Item>
</ListBox>
Empty state#
Use the renderEmptyState
prop to customize what the ListBox
will display if there are no items.
<ListBox
aria-label="Search results"
renderEmptyState={() => 'No results found.'}
>
{[]}
</ListBox>
<ListBox
aria-label="Search results"
renderEmptyState={() => 'No results found.'}
>
{[]}
</ListBox>
<ListBox
aria-label="Search results"
renderEmptyState={() =>
'No results found.'}
>
{[]}
</ListBox>
Drag and drop#
ListBox supports drag and drop interactions when the dragAndDropHooks
prop is provided using the useDragAndDrop
hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items.
React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets. A droppable collection is treated as a single drop target, so that users can easily tab past it to get to the next drop target. Within a droppable collection, keys such as ArrowDown and ArrowUp can be used to select a drop position, such as on an item, or between items.
See the drag and drop introduction to learn more.
Reorderable#
This example shows a basic list that allows users to reorder items via drag and drop. This is enabled using the onReorder
event handler, provided to the useDragAndDrop
hook. The getItems
function must also be implemented for items to become draggable. See below for more details.
This uses useListData from React Stately to manage the item list. Note that useListData
is a convenience hook, not a requirement. You can manage your state however you wish.
import {useListData} from 'react-stately';
import {useDragAndDrop} from 'react-aria-components';
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: 'Adobe Photoshop' },
{ id: 2, name: 'Adobe XD' },
{ id: 3, name: 'Adobe Dreamweaver' },
{ id: 4, name: 'Adobe InDesign' },
{ id: 5, name: 'Adobe Connect' }
]
});
let { dragAndDropHooks } = useDragAndDrop({
getItems: (keys) =>
[...keys].map((key) => ({ 'text/plain': list.getItem(key).name })),
onReorder(e) {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
}
});
return (
<ListBox
aria-label="Reorderable list"
selectionMode="multiple"
items={list.items}
dragAndDropHooks={dragAndDropHooks} >
{(item) => <Item>{item.name}</Item>}
</ListBox>
);
}
import {useListData} from 'react-stately';
import {useDragAndDrop} from 'react-aria-components';
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: 'Adobe Photoshop' },
{ id: 2, name: 'Adobe XD' },
{ id: 3, name: 'Adobe Dreamweaver' },
{ id: 4, name: 'Adobe InDesign' },
{ id: 5, name: 'Adobe Connect' }
]
});
let { dragAndDropHooks } = useDragAndDrop({
getItems: (keys) =>
[...keys].map((key) => ({
'text/plain': list.getItem(key).name
})),
onReorder(e) {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
}
});
return (
<ListBox
aria-label="Reorderable list"
selectionMode="multiple"
items={list.items}
dragAndDropHooks={dragAndDropHooks} >
{(item) => <Item>{item.name}</Item>}
</ListBox>
);
}
import {useListData} from 'react-stately';
import {useDragAndDrop} from 'react-aria-components';
function Example() {
let list = useListData(
{
initialItems: [
{
id: 1,
name:
'Adobe Photoshop'
},
{
id: 2,
name:
'Adobe XD'
},
{
id: 3,
name:
'Adobe Dreamweaver'
},
{
id: 4,
name:
'Adobe InDesign'
},
{
id: 5,
name:
'Adobe Connect'
}
]
}
);
let {
dragAndDropHooks
} = useDragAndDrop({
getItems: (keys) =>
[...keys].map(
(key) => ({
'text/plain':
list.getItem(
key
).name
})
),
onReorder(e) {
if (
e.target
.dropPosition ===
'before'
) {
list.moveBefore(
e.target.key,
e.keys
);
} else if (
e.target
.dropPosition ===
'after'
) {
list.moveAfter(
e.target.key,
e.keys
);
}
}
});
return (
<ListBox
aria-label="Reorderable list"
selectionMode="multiple"
items={list.items}
dragAndDropHooks={dragAndDropHooks} >
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListBox>
);
}
Show CSS
.react-aria-Item {
&[data-dragging] {
opacity: 0.6;
}
[slot=drag] {
all: unset;
width: 15px;
text-align: center;
&[data-focus-visible] {
border-radius: 4px;
box-shadow: 0 0 0 2px var(--highlight-background);
}
}
&[aria-selected=true] {
[slot=drag][data-focus-visible] {
box-shadow: 0 0 0 2px var(--highlight-foreground);
}
}
}
.react-aria-DropIndicator[data-drop-target] {
outline: 1px solid var(--highlight-background);
}
.react-aria-Item {
&[data-dragging] {
opacity: 0.6;
}
[slot=drag] {
all: unset;
width: 15px;
text-align: center;
&[data-focus-visible] {
border-radius: 4px;
box-shadow: 0 0 0 2px var(--highlight-background);
}
}
&[aria-selected=true] {
[slot=drag][data-focus-visible] {
box-shadow: 0 0 0 2px var(--highlight-foreground);
}
}
}
.react-aria-DropIndicator[data-drop-target] {
outline: 1px solid var(--highlight-background);
}
.react-aria-Item {
&[data-dragging] {
opacity: 0.6;
}
[slot=drag] {
all: unset;
width: 15px;
text-align: center;
&[data-focus-visible] {
border-radius: 4px;
box-shadow: 0 0 0 2px var(--highlight-background);
}
}
&[aria-selected=true] {
[slot=drag][data-focus-visible] {
box-shadow: 0 0 0 2px var(--highlight-foreground);
}
}
}
.react-aria-DropIndicator[data-drop-target] {
outline: 1px solid var(--highlight-background);
}
Custom drag preview#
By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered by implementing the renderDragPreview
function, passed to useDragAndDrop
. This receives the dragged data that was returned by getItems
, and returns a rendered preview for those items.
This example renders a custom drag preview which shows the number of items being dragged.
import {useListData} from 'react-stately';
import {useDragAndDrop} from 'react-aria-components';
function Example() {
let {dragAndDropHooks} = useDragAndDrop({
// ...
renderDragPreview(items) {
return (
<div className="drag-preview">
{items[0]['text/plain']}
<span className="badge">{items.length}</span>
</div>
);
} });
// ...
}
import {useListData} from 'react-stately';
import {useDragAndDrop} from 'react-aria-components';
function Example() {
let {dragAndDropHooks} = useDragAndDrop({
// ...
renderDragPreview(items) {
return (
<div className="drag-preview">
{items[0]['text/plain']}
<span className="badge">{items.length}</span>
</div>
);
} });
// ...
}
import {useListData} from 'react-stately';
import {useDragAndDrop} from 'react-aria-components';
function Example() {
let {
dragAndDropHooks
} = useDragAndDrop({
// ...
renderDragPreview(
items
) {
return (
<div className="drag-preview">
{items[0][
'text/plain'
]}
<span className="badge">
{items
.length}
</span>
</div>
);
} });
// ...
}
Show CSS
.drag-preview {
width: 150px;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
background: slateblue;
color: white;
border-radius: 4px;
.badge {
background: white;
color: slateblue;
padding: 0 8px;
border-radius: 4px;
}
}
.drag-preview {
width: 150px;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
background: slateblue;
color: white;
border-radius: 4px;
.badge {
background: white;
color: slateblue;
padding: 0 8px;
border-radius: 4px;
}
}
.drag-preview {
width: 150px;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
background: slateblue;
color: white;
border-radius: 4px;
.badge {
background: white;
color: slateblue;
padding: 0 8px;
border-radius: 4px;
}
}
Drag data#
Data for draggable items can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message).
This can be done by returning multiple keys for an item from the getItems
function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app.
This example provides representations of each item as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format.
function DraggableListBox() {
let items = new Map([
['ps', { name: 'Photoshop', style: 'strong' }],
['xd', { name: 'XD', style: 'strong' }],
['id', { name: 'InDesign', style: 'strong' }],
['dw', { name: 'Dreamweaver', style: 'em' }],
['co', { name: 'Connect', style: 'em' }]
]);
let { dragAndDropHooks } = useDragAndDrop({
getItems(keys) {
return [...keys].map((key) => {
let item = items.get(key as string)!;
return {
'text/plain': item.name,
'text/html': `< > </ >`,
'custom-app-type': JSON.stringify({ id: key, ...item })
};
});
} });
return (
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={items}
dragAndDropHooks={dragAndDropHooks}
>
{([id, item]) => (
<Item id={id} textValue={item.name}>
{React.createElement(item.style || 'span', null, item.name)}
</Item>
)}
</ListBox>
);
}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<DraggableListBox />
{/* see below */}
<DroppableListBox />
</div>
function DraggableListBox() {
let items = new Map([
['ps', { name: 'Photoshop', style: 'strong' }],
['xd', { name: 'XD', style: 'strong' }],
['id', { name: 'InDesign', style: 'strong' }],
['dw', { name: 'Dreamweaver', style: 'em' }],
['co', { name: 'Connect', style: 'em' }]
]);
let { dragAndDropHooks } = useDragAndDrop({
getItems(keys) {
return [...keys].map((key) => {
let item = items.get(key as string)!;
return {
'text/plain': item.name,
'text/html':
`< > </ >`,
'custom-app-type': JSON.stringify({
id: key,
...item
})
};
});
} });
return (
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={items}
dragAndDropHooks={dragAndDropHooks}
>
{([id, item]) => (
<Item id={id} textValue={item.name}>
{React.createElement(
item.style || 'span',
null,
item.name
)}
</Item>
)}
</ListBox>
);
}
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
<DraggableListBox />
{/* see below */}
<DroppableListBox />
</div>
function DraggableListBox() {
let items = new Map([
['ps', {
name: 'Photoshop',
style: 'strong'
}],
['xd', {
name: 'XD',
style: 'strong'
}],
['id', {
name: 'InDesign',
style: 'strong'
}],
['dw', {
name:
'Dreamweaver',
style: 'em'
}],
['co', {
name: 'Connect',
style: 'em'
}]
]);
let {
dragAndDropHooks
} = useDragAndDrop({
getItems(keys) {
return [...keys]
.map((key) => {
let item =
items.get(
key as string
)!;
return {
'text/plain':
item.name,
'text/html':
`< > </ >`,
'custom-app-type':
JSON
.stringify(
{
id:
key,
...item
}
)
};
});
} });
return (
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={items}
dragAndDropHooks={dragAndDropHooks}
>
{([id, item]) => (
<Item
id={id}
textValue={item
.name}
>
{React
.createElement(
item
.style ||
'span',
null,
item.name
)}
</Item>
)}
</ListBox>
);
}
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
<DraggableListBox />
{/* see below */}
<DroppableListBox />
</div>
Dropping on the collection#
Dropping on the ListBox as a whole can be enabled using the onRootDrop
event. When a valid drag hovers over the ListBox, it receives the isDropTarget
state and can be styled using the [data-drop-target]
CSS selector.
interface Item {
id: number;
name: string;
}
function Example() {
let [items, setItems] = React.useState<Item[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
async onRootDrop(e) {
let items = await Promise.all(e.items.map(async (item, i) => {
let name = item.kind === 'text'
? await item.getText('text/plain')
: item.name;
return { id: i, name };
}));
setItems(items);
} });
return (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<DraggableListBox />
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
</div>
);
}
interface Item {
id: number;
name: string;
}
function Example() {
let [items, setItems] = React.useState<Item[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
async onRootDrop(e) {
let items = await Promise.all(
e.items.map(async (item, i) => {
let name = item.kind === 'text'
? await item.getText('text/plain')
: item.name;
return { id: i, name };
})
);
setItems(items);
} });
return (
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
<DraggableListBox />
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
</div>
);
}
interface Item {
id: number;
name: string;
}
function Example() {
let [items, setItems] =
React.useState<
Item[]
>([]);
let {
dragAndDropHooks
} = useDragAndDrop({
async onRootDrop(e) {
let items =
await Promise
.all(
e.items.map(
async (
item,
i
) => {
let name =
item
.kind ===
'text'
? await item
.getText(
'text/plain'
)
: item
.name;
return {
id: i,
name
};
}
)
);
setItems(items);
} });
return (
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
<DraggableListBox />
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() =>
'Drop items here'}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListBox>
</div>
);
}
Show CSS
.react-aria-ListBox[data-drop-target] {
border-color: var(--highlight-background);
box-shadow: 0 0 0 1px var(--highlight-background);
background: rgb(from slateblue r g b / 15%);
}
.react-aria-ListBox[data-drop-target] {
border-color: var(--highlight-background);
box-shadow: 0 0 0 1px var(--highlight-background);
background: rgb(from slateblue r g b / 15%);
}
.react-aria-ListBox[data-drop-target] {
border-color: var(--highlight-background);
box-shadow: 0 0 0 1px var(--highlight-background);
background: rgb(from slateblue r g b / 15%);
}
Dropping on items#
Dropping on items can be enabled using the onItemDrop
event. When a valid drag hovers over an item, it receives the isDropTarget
state and can be styled using the [data-drop-target]
CSS selector.
function Example() {
let { dragAndDropHooks } = useDragAndDrop({
onItemDrop(e) {
alert(`Dropped on `);
} });
return (
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}>
{/* see above */}
<DraggableListBox />
<ListBox aria-label="Droppable list" dragAndDropHooks={dragAndDropHooks}>
<Item id="applications">Applications</Item>
<Item id="documents">Documents</Item>
<Item id="pictures">Pictures</Item>
</ListBox>
</div>
);
}
function Example() {
let { dragAndDropHooks } = useDragAndDrop({
onItemDrop(e) {
alert(`Dropped on `);
} });
return (
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
{/* see above */}
<DraggableListBox />
<ListBox
aria-label="Droppable list"
dragAndDropHooks={dragAndDropHooks}
>
<Item id="applications">Applications</Item>
<Item id="documents">Documents</Item>
<Item id="pictures">Pictures</Item>
</ListBox>
</div>
);
}
function Example() {
let {
dragAndDropHooks
} = useDragAndDrop({
onItemDrop(e) {
alert(
`Dropped on
`);
} });
return (
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
{/* see above */}
<DraggableListBox />
<ListBox
aria-label="Droppable list"
dragAndDropHooks={dragAndDropHooks}
>
<Item id="applications">
Applications
</Item>
<Item id="documents">
Documents
</Item>
<Item id="pictures">
Pictures
</Item>
</ListBox>
</div>
);
}
Show CSS
.react-aria-Item[data-drop-target] {
box-shadow: inset 0 0 0 2px var(--highlight-background);
background: rgb(from slateblue r g b / 15%);
}
.react-aria-Item[data-drop-target] {
box-shadow: inset 0 0 0 2px var(--highlight-background);
background: rgb(from slateblue r g b / 15%);
}
.react-aria-Item[data-drop-target] {
box-shadow: inset 0 0 0 2px var(--highlight-background);
background: rgb(from slateblue r g b / 15%);
}
Dropping between items#
Dropping between items can be enabled using the onInsert
event. ListBox renders a DropIndicator
between items to indicate the insertion position, which can be styled using the .react-aria-DropIndicator
selector. When it is active, it receives the [data-drop-target]
state.
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: 'Illustrator' },
{ id: 2, name: 'Premiere' },
{ id: 3, name: 'Acrobat' }
]
});
let { dragAndDropHooks } = useDragAndDrop({
async onInsert(e) {
let items = await Promise.all(e.items.map(async (item) => {
let name = item.kind === 'text'
? await item.getText('text/plain')
: item.name;
return { id: Math.random(), name };
}));
if (e.target.dropPosition === 'before') {
list.insertBefore(e.target.key, ...items);
} else if (e.target.dropPosition === 'after') {
list.insertAfter(e.target.key, ...items);
}
} });
return (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<DraggableListBox />
<ListBox
aria-label="Droppable list"
items={list.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
</div>
);
}
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: 'Illustrator' },
{ id: 2, name: 'Premiere' },
{ id: 3, name: 'Acrobat' }
]
});
let { dragAndDropHooks } = useDragAndDrop({
async onInsert(e) {
let items = await Promise.all(
e.items.map(async (item) => {
let name = item.kind === 'text'
? await item.getText('text/plain')
: item.name;
return { id: Math.random(), name };
})
);
if (e.target.dropPosition === 'before') {
list.insertBefore(e.target.key, ...items);
} else if (e.target.dropPosition === 'after') {
list.insertAfter(e.target.key, ...items);
}
} });
return (
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
<DraggableListBox />
<ListBox
aria-label="Droppable list"
items={list.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
</div>
);
}
function Example() {
let list = useListData(
{
initialItems: [
{
id: 1,
name:
'Illustrator'
},
{
id: 2,
name:
'Premiere'
},
{
id: 3,
name: 'Acrobat'
}
]
}
);
let {
dragAndDropHooks
} = useDragAndDrop({
async onInsert(e) {
let items =
await Promise
.all(
e.items.map(
async (item) => {
let name =
item
.kind ===
'text'
? await item
.getText(
'text/plain'
)
: item
.name;
return {
id:
Math
.random(),
name
};
}
)
);
if (
e.target
.dropPosition ===
'before'
) {
list
.insertBefore(
e.target.key,
...items
);
} else if (
e.target
.dropPosition ===
'after'
) {
list.insertAfter(
e.target.key,
...items
);
}
} });
return (
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
<DraggableListBox />
<ListBox
aria-label="Droppable list"
items={list
.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListBox>
</div>
);
}
Show CSS
.react-aria-DropIndicator[data-drop-target] {
outline: 1px solid var(--highlight-background);
}
.react-aria-DropIndicator[data-drop-target] {
outline: 1px solid var(--highlight-background);
}
.react-aria-DropIndicator[data-drop-target] {
outline: 1px solid var(--highlight-background);
}
A custom drop indicator can also be rendered with the renderDropIndicator
function. This lets you customize the DOM structure and CSS classes applied to the drop indicator.
import {DropIndicator} from 'react-aria-components';
function Example() {
let { dragAndDropHooks } = useDragAndDrop({
// ...
renderDropIndicator(target) {
return (
<DropIndicator
target={target}
className={({ isDropTarget }) =>
`my-drop-indicator `}
/>
);
} });
// ...
}
import {DropIndicator} from 'react-aria-components';
function Example() {
let { dragAndDropHooks } = useDragAndDrop({
// ...
renderDropIndicator(target) {
return (
<DropIndicator
target={target}
className={({ isDropTarget }) =>
`my-drop-indicator `}
/>
);
} });
// ...
}
import {DropIndicator} from 'react-aria-components';
function Example() {
let {
dragAndDropHooks
} = useDragAndDrop({
// ...
renderDropIndicator(
target
) {
return (
<DropIndicator
target={target}
className={(
{
isDropTarget
}
) =>
`my-drop-indicator `}
/>
);
} });
// ...
}
Show CSS
.my-drop-indicator.active {
outline: 1px solid #e70073;
}
.my-drop-indicator.active {
outline: 1px solid #e70073;
}
.my-drop-indicator.active {
outline: 1px solid #e70073;
}
Drop data#
ListBox
allows users to drop one or more drag items, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items:
text
– represents data inline as a string in one or more formatsfile
– references a file on the user's devicedirectory
– references the contents of a directory
Text#
A TextDropItem
represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application.
The example below uses the acceptedDragTypes
prop to accept items that include a custom app-specific type, which is retrieved using the item's getText
method. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. When acceptedDragTypes
is specified, the dropped items are filtered to include only items that include the accepted types.
import {isTextDropItem} from 'react-aria-components';
interface TextItem {
id: string;
name: string;
style: string;
}
function DroppableListBox() {
let [items, setItems] = React.useState<TextItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
acceptedDragTypes: ['custom-app-type'],
async onRootDrop(e) {
let items = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async (item) =>
JSON.parse(await item.getText('custom-app-type'))
)
);
setItems(items);
} });
return (
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => (
<Item textValue={item.name}>
{React.createElement(item.style || 'span', null, item.name)}
</Item>
)}
</ListBox>
);
}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{/* see above */}
<DraggableListBox />
<DroppableListBox />
</div>
import {isTextDropItem} from 'react-aria-components';
interface TextItem {
id: string;
name: string;
style: string;
}
function DroppableListBox() {
let [items, setItems] = React.useState<TextItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
acceptedDragTypes: ['custom-app-type'],
async onRootDrop(e) {
let items = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async (item) =>
JSON.parse(
await item.getText('custom-app-type')
)
)
);
setItems(items);
} });
return (
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => (
<Item textValue={item.name}>
{React.createElement(
item.style || 'span',
null,
item.name
)}
</Item>
)}
</ListBox>
);
}
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
{/* see above */}
<DraggableListBox />
<DroppableListBox />
</div>
import {isTextDropItem} from 'react-aria-components';
interface TextItem {
id: string;
name: string;
style: string;
}
function DroppableListBox() {
let [items, setItems] =
React.useState<
TextItem[]
>([]);
let {
dragAndDropHooks
} = useDragAndDrop({
acceptedDragTypes: [
'custom-app-type'
],
async onRootDrop(e) {
let items =
await Promise
.all(
e.items
.filter(
isTextDropItem
)
.map(
async (item) =>
JSON
.parse(
await item
.getText(
'custom-app-type'
)
)
)
);
setItems(items);
} });
return (
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() =>
'Drop items here'}
>
{(item) => (
<Item
textValue={item
.name}
>
{React
.createElement(
item
.style ||
'span',
null,
item.name
)}
</Item>
)}
</ListBox>
);
}
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
{/* see above */}
<DraggableListBox />
<DroppableListBox />
</div>
Files#
A FileDropItem
references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading.
This example accepts JPEG and PNG image files, and renders them by creating a local object URL. When the list is empty, you can drop on the whole collection, and otherwise items can be inserted.
import {isFileDropItem} from 'react-aria-components';
interface ImageItem {
id: number;
url: string;
name: string;
}
function Example() {
let [items, setItems] = React.useState<ImageItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
acceptedDragTypes: ['image/jpeg', 'image/png'],
async onRootDrop(e) {
let items = await Promise.all(
e.items.filter(isFileDropItem).map(async (item) => ({
id: Math.random(),
url: URL.createObjectURL(await item.getFile()),
name: item.name
}))
);
setItems(items);
} });
return (
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => (
<Item textValue={item.name}>
<div className="image-item">
<img src={item.url} />
<span>{item.name}</span>
</div>
</Item>
)}
</ListBox>
);
}
import {isFileDropItem} from 'react-aria-components';
interface ImageItem {
id: number;
url: string;
name: string;
}
function Example() {
let [items, setItems] = React.useState<ImageItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
acceptedDragTypes: ['image/jpeg', 'image/png'],
async onRootDrop(e) {
let items = await Promise.all(
e.items.filter(isFileDropItem).map(
async (item) => ({
id: Math.random(),
url: URL.createObjectURL(await item.getFile()),
name: item.name
})
)
);
setItems(items);
} });
return (
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => (
<Item textValue={item.name}>
<div className="image-item">
<img src={item.url} />
<span>{item.name}</span>
</div>
</Item>
)}
</ListBox>
);
}
import {isFileDropItem} from 'react-aria-components';
interface ImageItem {
id: number;
url: string;
name: string;
}
function Example() {
let [items, setItems] =
React.useState<
ImageItem[]
>([]);
let {
dragAndDropHooks
} = useDragAndDrop({
acceptedDragTypes: [
'image/jpeg',
'image/png'
],
async onRootDrop(e) {
let items =
await Promise
.all(
e.items
.filter(
isFileDropItem
).map(
async (item) => ({
id:
Math
.random(),
url:
URL
.createObjectURL(
await item
.getFile()
),
name:
item
.name
})
)
);
setItems(items);
} });
return (
<ListBox
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() =>
'Drop items here'}
>
{(item) => (
<Item
textValue={item
.name}
>
<div className="image-item">
<img
src={item
.url}
/>
<span>
{item.name}
</span>
</div>
</Item>
)}
</ListBox>
);
}
Show CSS
.image-item {
display: flex;
height: 50px;
gap: 10px;
align-items: center;
}
.image-item img {
height: 100%;
aspect-ratio: 1/1;
object-fit: contain;
}
.image-item span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-item {
display: flex;
height: 50px;
gap: 10px;
align-items: center;
}
.image-item img {
height: 100%;
aspect-ratio: 1/1;
object-fit: contain;
}
.image-item span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-item {
display: flex;
height: 50px;
gap: 10px;
align-items: center;
}
.image-item img {
height: 100%;
aspect-ratio: 1/1;
object-fit: contain;
}
.image-item span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
Directories#
A DirectoryDropItem
references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively.
The getEntries
method returns an async iterable object, which can be used in a for await...of
loop. This provides each item in the directory as either a FileDropItem
or DirectoryDropItem
, and you can access the contents of each file as discussed above.
This example accepts directory drops over the whole collection, and renders the contents as items in the list. DIRECTORY_DRAG_TYPE
is imported from react-aria-components
and included in the acceptedDragTypes
prop to limit the accepted items to only directories.
import File from '@spectrum-icons/workflow/FileTxt';
import Folder from '@spectrum-icons/workflow/Folder';
import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components';
interface DirItem {
name: string;
kind: string;
}
function Example() {
let [files, setFiles] = React.useState<DirItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
acceptedDragTypes: [DIRECTORY_DRAG_TYPE],
async onRootDrop(e) {
// Read entries in directory and update state with relevant info.
let dir = e.items.find(isDirectoryDropItem)!;
let files = [];
for await (let entry of dir.getEntries()) {
files.push({
name: entry.name,
kind: entry.kind
});
}
setFiles(files);
} });
return (
<ListBox
aria-label="Droppable list"
items={files}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => (
<Item id={item.name} textValue={item.name}>
<div className="dir-item">
{item.kind === 'directory' ? <Folder /> : <File />}
<span>{item.name}</span>
</div>
</Item>
)}
</ListBox>
);
}
import File from '@spectrum-icons/workflow/FileTxt';
import Folder from '@spectrum-icons/workflow/Folder';
import {
DIRECTORY_DRAG_TYPE,
isDirectoryDropItem
} from 'react-aria-components';
interface DirItem {
name: string;
kind: string;
}
function Example() {
let [files, setFiles] = React.useState<DirItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
acceptedDragTypes: [DIRECTORY_DRAG_TYPE],
async onRootDrop(e) {
// Read entries in directory and update state with relevant info.
let dir = e.items.find(isDirectoryDropItem)!;
let files = [];
for await (let entry of dir.getEntries()) {
files.push({
name: entry.name,
kind: entry.kind
});
}
setFiles(files);
} });
return (
<ListBox
aria-label="Droppable list"
items={files}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => (
<Item id={item.name} textValue={item.name}>
<div className="dir-item">
{item.kind === 'directory'
? <Folder />
: <File />}
<span>{item.name}</span>
</div>
</Item>
)}
</ListBox>
);
}
import File from '@spectrum-icons/workflow/FileTxt';
import Folder from '@spectrum-icons/workflow/Folder';
import {
DIRECTORY_DRAG_TYPE,
isDirectoryDropItem
} from 'react-aria-components';
interface DirItem {
name: string;
kind: string;
}
function Example() {
let [files, setFiles] =
React.useState<
DirItem[]
>([]);
let {
dragAndDropHooks
} = useDragAndDrop({
acceptedDragTypes: [
DIRECTORY_DRAG_TYPE
],
async onRootDrop(e) {
// Read entries in directory and update state with relevant info.
let dir = e.items
.find(
isDirectoryDropItem
)!;
let files = [];
for await (
let entry of dir
.getEntries()
) {
files.push({
name:
entry.name,
kind:
entry.kind
});
}
setFiles(files);
} });
return (
<ListBox
aria-label="Droppable list"
items={files}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() =>
'Drop items here'}
>
{(item) => (
<Item
id={item.name}
textValue={item
.name}
>
<div className="dir-item">
{item
.kind ===
'directory'
? (
<Folder />
)
: <File />}
<span>
{item.name}
</span>
</div>
</Item>
)}
</ListBox>
);
}
Show CSS
.dir-item {
display: flex;
align-items: center;
gap: 8px;
}
.dir-item {
flex: 0 0 auto;
}
.dir-item {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dir-item {
display: flex;
align-items: center;
gap: 8px;
}
.dir-item {
flex: 0 0 auto;
}
.dir-item {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dir-item {
display: flex;
align-items: center;
gap: 8px;
}
.dir-item {
flex: 0 0 auto;
}
.dir-item {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
Drop operations#
A DropOperation
is an indication of what will happen when dragged data is dropped on a particular drop target. These are:
move
– indicates that the dragged data will be moved from its source location to the target location.copy
– indicates that the dragged data will be copied to the target destination.link
– indicates that there will be a relationship established between the source and target locations.cancel
– indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target.
Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy.
onDragEnd#
The onDragEnd
event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The dropOperation
property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged items.
This example removes the dragged items from the UI when a move operation is completed. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes.
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: 'Adobe Photoshop' },
{ id: 2, name: 'Adobe XD' },
{ id: 3, name: 'Adobe Dreamweaver' },
{ id: 4, name: 'Adobe InDesign' },
{ id: 5, name: 'Adobe Connect' }
]
});
let { dragAndDropHooks } = useDragAndDrop({
// ...
onDragEnd(e) {
if (e.dropOperation === 'move') {
list.remove(...e.keys);
}
} });
return (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={list.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
<DroppableListBox />
</div>
);
}
function Example() {
let list = useListData({
initialItems: [
{ id: 1, name: 'Adobe Photoshop' },
{ id: 2, name: 'Adobe XD' },
{ id: 3, name: 'Adobe Dreamweaver' },
{ id: 4, name: 'Adobe InDesign' },
{ id: 5, name: 'Adobe Connect' }
]
});
let { dragAndDropHooks } = useDragAndDrop({
// ...
onDragEnd(e) {
if (e.dropOperation === 'move') {
list.remove(...e.keys);
}
} });
return (
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={list.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
<DroppableListBox />
</div>
);
}
function Example() {
let list = useListData(
{
initialItems: [
{
id: 1,
name:
'Adobe Photoshop'
},
{
id: 2,
name:
'Adobe XD'
},
{
id: 3,
name:
'Adobe Dreamweaver'
},
{
id: 4,
name:
'Adobe InDesign'
},
{
id: 5,
name:
'Adobe Connect'
}
]
}
);
let {
dragAndDropHooks
} = useDragAndDrop({
// ...
onDragEnd(e) {
if (
e.dropOperation ===
'move'
) {
list.remove(
...e.keys
);
}
} });
return (
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={list
.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListBox>
<DroppableListBox />
</div>
);
}
getAllowedDropOperations#
The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the getAllowedDropOperations
function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled.
function Example() {
// ...
let { dragAndDropHooks } = useDragAndDrop({
// ...
getAllowedDropOperations: () => ['copy'] });
return (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={list.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
<DroppableListBox />
</div>
);
}
function Example() {
// ...
let { dragAndDropHooks } = useDragAndDrop({
// ...
getAllowedDropOperations: () => ['copy'] });
return (
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={list.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
<DroppableListBox />
</div>
);
}
function Example() {
// ...
let {
dragAndDropHooks
} = useDragAndDrop({
// ...
getAllowedDropOperations:
() => ['copy'] });
return (
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
<ListBox
aria-label="Draggable list"
selectionMode="multiple"
items={list
.items}
dragAndDropHooks={dragAndDropHooks}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListBox>
<DroppableListBox />
</div>
);
}
getDropOperation#
The getDropOperation
function passed to useDragAndDrop
can be used to provide appropriate feedback to the user when a drag hovers over the drop target. This function receives the drop target, set of types contained in the drag, and a list of allowed drop operations as specified by the drag source. It should return one of the drop operations in allowedOperations
, or a specific drop operation if only that drop operation is supported. It may also return 'cancel'
to reject the drop. If the returned operation is not in allowedOperations
, then the drop target will act as if 'cancel'
was returned.
In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop.
function Example() {
let [items, setItems] = React.useState<ImageItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'],
async onRootDrop(e) {
// ...
}
});
// See "Files" example above...
}
function Example() {
let [items, setItems] = React.useState<ImageItem[]>([]);
let { dragAndDropHooks } = useDragAndDrop({
getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'],
async onRootDrop(e) {
// ...
}
});
// See "Files" example above...
}
function Example() {
let [items, setItems] =
React.useState<
ImageItem[]
>([]);
let {
dragAndDropHooks
} = useDragAndDrop({
getDropOperation:
() => 'copy', acceptedDragTypes: [
'image/png'
],
async onRootDrop(e) {
// ...
}
});
// See "Files" example above...
}
Drop events#
Drop events such as onInsert
, onItemDrop
, etc. also include the dropOperation
. This can be used to perform different actions accordingly, for example, when communicating with a backend API.
let onItemDrop = async (e) => {
let data = JSON.parse(await e.items[0].getText('my-app-file'));
switch (e.dropOperation) {
case 'move':
MyAppFileService.move(data.filePath, props.filePath);
break;
case 'copy':
MyAppFileService.copy(data.filePath, props.filePath);
break;
case 'link':
MyAppFileService.link(data.filePath, props.filePath);
break;
}};
let onItemDrop = async (e) => {
let data = JSON.parse(
await e.items[0].getText('my-app-file')
);
switch (e.dropOperation) {
case 'move':
MyAppFileService.move(data.filePath, props.filePath);
break;
case 'copy':
MyAppFileService.copy(data.filePath, props.filePath);
break;
case 'link':
MyAppFileService.link(data.filePath, props.filePath);
break;
}};
let onItemDrop = async (
e
) => {
let data = JSON.parse(
await e.items[0]
.getText(
'my-app-file'
)
);
switch (
e.dropOperation
) {
case 'move':
MyAppFileService
.move(
data.filePath,
props.filePath
);
break;
case 'copy':
MyAppFileService
.copy(
data.filePath,
props.filePath
);
break;
case 'link':
MyAppFileService
.link(
data.filePath,
props.filePath
);
break;
}};
Drag between lists#
This example puts together many of the concepts described above, allowing users to drag items between lists bidirectionally. It also supports reordering items within the same list. When a list is empty, it accepts drops on the whole collection. getDropOperation
ensures that items are always moved rather than copied, which avoids duplicate items.
import {isTextDropItem} from 'react-aria-components';
interface FileItem {
id: string,
name: string,
type: string
}
interface DndListBoxProps {
initialItems: FileItem[],
'aria-label': string
}
function DndListBox(props: DndListBoxProps) {
let list = useListData({
initialItems: props.initialItems
});
let { dragAndDropHooks } = useDragAndDrop({
// Provide drag data in a custom format as well as plain text.
getItems(keys) {
return [...keys].map((key) => {
let item = list.getItem(key);
return {
'custom-app-type': JSON.stringify(item),
'text/plain': item.name
};
});
},
// Accept drops with the custom format.
acceptedDragTypes: ['custom-app-type'],
// Ensure items are always moved rather than copied.
getDropOperation: () => 'move',
// Handle drops between items from other lists.
async onInsert(e) {
let processedItems = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async item => JSON.parse(await item.getText('custom-app-type')))
);
if (e.target.dropPosition === 'before') {
list.insertBefore(e.target.key, ...processedItems);
} else if (e.target.dropPosition === 'after') {
list.insertAfter(e.target.key, ...processedItems);
}
},
// Handle drops on the collection when empty.
async onRootDrop(e) {
let processedItems = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async item => JSON.parse(await item.getText('custom-app-type')))
);
list.append(...processedItems);
},
// Handle reordering items within the same list.
onReorder(e) {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
},
// Remove the items from the source list on drop
// if they were moved to a different list.
onDragEnd(e) {
if (e.dropOperation === 'move' && !e.isInternal) {
list.remove(...e.keys);
}
}
});
return (
<ListBox
aria-label={props['aria-label']}
selectionMode="multiple"
selectedKeys={list.selectedKeys}
onSelectionChange={list.setSelectedKeys}
items={list.items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}>
{item => <Item>{item.name}</Item>}
</ListBox>
);
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}>
<DndListBox
initialItems={[
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'folder', name: 'Documents' },
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{ id: '5', type: 'folder', name: 'Utilities' },
{ id: '6', type: 'file', name: 'Adobe AfterEffects' }
]}
aria-label="First ListBox"
/>
<DndListBox
initialItems={[
{ id: '7', type: 'folder', name: 'Pictures' },
{ id: '8', type: 'file', name: 'Adobe Fresco' },
{ id: '9', type: 'folder', name: 'Apps' },
{ id: '10', type: 'file', name: 'Adobe Illustrator' },
{ id: '11', type: 'file', name: 'Adobe Lightroom' },
{ id: '12', type: 'file', name: 'Adobe Dreamweaver' }
]}
aria-label="Second ListBox"
/>
</div>
import {isTextDropItem} from 'react-aria-components';
interface FileItem {
id: string;
name: string;
type: string;
}
interface DndListBoxProps {
initialItems: FileItem[];
'aria-label': string;
}
function DndListBox(props: DndListBoxProps) {
let list = useListData({
initialItems: props.initialItems
});
let { dragAndDropHooks } = useDragAndDrop({
// Provide drag data in a custom format as well as plain text.
getItems(keys) {
return [...keys].map((key) => {
let item = list.getItem(key);
return {
'custom-app-type': JSON.stringify(item),
'text/plain': item.name
};
});
},
// Accept drops with the custom format.
acceptedDragTypes: ['custom-app-type'],
// Ensure items are always moved rather than copied.
getDropOperation: () => 'move',
// Handle drops between items from other lists.
async onInsert(e) {
let processedItems = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async (item) =>
JSON.parse(
await item.getText('custom-app-type')
)
)
);
if (e.target.dropPosition === 'before') {
list.insertBefore(e.target.key, ...processedItems);
} else if (e.target.dropPosition === 'after') {
list.insertAfter(e.target.key, ...processedItems);
}
},
// Handle drops on the collection when empty.
async onRootDrop(e) {
let processedItems = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async (item) =>
JSON.parse(
await item.getText('custom-app-type')
)
)
);
list.append(...processedItems);
},
// Handle reordering items within the same list.
onReorder(e) {
if (e.target.dropPosition === 'before') {
list.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
list.moveAfter(e.target.key, e.keys);
}
},
// Remove the items from the source list on drop
// if they were moved to a different list.
onDragEnd(e) {
if (e.dropOperation === 'move' && !e.isInternal) {
list.remove(...e.keys);
}
}
});
return (
<ListBox
aria-label={props['aria-label']}
selectionMode="multiple"
selectedKeys={list.selectedKeys}
onSelectionChange={list.setSelectedKeys}
items={list.items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop items here'}
>
{(item) => <Item>{item.name}</Item>}
</ListBox>
);
}
<div
style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}
>
<DndListBox
initialItems={[
{ id: '1', type: 'file', name: 'Adobe Photoshop' },
{ id: '2', type: 'file', name: 'Adobe XD' },
{ id: '3', type: 'folder', name: 'Documents' },
{ id: '4', type: 'file', name: 'Adobe InDesign' },
{ id: '5', type: 'folder', name: 'Utilities' },
{
id: '6',
type: 'file',
name: 'Adobe AfterEffects'
}
]}
aria-label="First ListBox"
/>
<DndListBox
initialItems={[
{ id: '7', type: 'folder', name: 'Pictures' },
{ id: '8', type: 'file', name: 'Adobe Fresco' },
{ id: '9', type: 'folder', name: 'Apps' },
{
id: '10',
type: 'file',
name: 'Adobe Illustrator'
},
{ id: '11', type: 'file', name: 'Adobe Lightroom' },
{
id: '12',
type: 'file',
name: 'Adobe Dreamweaver'
}
]}
aria-label="Second ListBox"
/>
</div>
import {isTextDropItem} from 'react-aria-components';
interface FileItem {
id: string;
name: string;
type: string;
}
interface DndListBoxProps {
initialItems:
FileItem[];
'aria-label': string;
}
function DndListBox(
props: DndListBoxProps
) {
let list = useListData(
{
initialItems:
props
.initialItems
}
);
let {
dragAndDropHooks
} = useDragAndDrop({
// Provide drag data in a custom format as well as plain text.
getItems(keys) {
return [...keys]
.map((key) => {
let item = list
.getItem(
key
);
return {
'custom-app-type':
JSON
.stringify(
item
),
'text/plain':
item.name
};
});
},
// Accept drops with the custom format.
acceptedDragTypes: [
'custom-app-type'
],
// Ensure items are always moved rather than copied.
getDropOperation:
() => 'move',
// Handle drops between items from other lists.
async onInsert(e) {
let processedItems =
await Promise
.all(
e.items
.filter(
isTextDropItem
)
.map(
async (item) =>
JSON
.parse(
await item
.getText(
'custom-app-type'
)
)
)
);
if (
e.target
.dropPosition ===
'before'
) {
list
.insertBefore(
e.target.key,
...processedItems
);
} else if (
e.target
.dropPosition ===
'after'
) {
list.insertAfter(
e.target.key,
...processedItems
);
}
},
// Handle drops on the collection when empty.
async onRootDrop(e) {
let processedItems =
await Promise
.all(
e.items
.filter(
isTextDropItem
)
.map(
async (item) =>
JSON
.parse(
await item
.getText(
'custom-app-type'
)
)
)
);
list.append(
...processedItems
);
},
// Handle reordering items within the same list.
onReorder(e) {
if (
e.target
.dropPosition ===
'before'
) {
list.moveBefore(
e.target.key,
e.keys
);
} else if (
e.target
.dropPosition ===
'after'
) {
list.moveAfter(
e.target.key,
e.keys
);
}
},
// Remove the items from the source list on drop
// if they were moved to a different list.
onDragEnd(e) {
if (
e.dropOperation ===
'move' &&
!e.isInternal
) {
list.remove(
...e.keys
);
}
}
});
return (
<ListBox
aria-label={props[
'aria-label'
]}
selectionMode="multiple"
selectedKeys={list
.selectedKeys}
onSelectionChange={list
.setSelectedKeys}
items={list.items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() =>
'Drop items here'}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</ListBox>
);
}
<div
style={{
display: 'flex',
gap: 12,
flexWrap: 'wrap'
}}
>
<DndListBox
initialItems={[
{
id: '1',
type: 'file',
name:
'Adobe Photoshop'
},
{
id: '2',
type: 'file',
name:
'Adobe XD'
},
{
id: '3',
type: 'folder',
name:
'Documents'
},
{
id: '4',
type: 'file',
name:
'Adobe InDesign'
},
{
id: '5',
type: 'folder',
name:
'Utilities'
},
{
id: '6',
type: 'file',
name:
'Adobe AfterEffects'
}
]}
aria-label="First ListBox"
/>
<DndListBox
initialItems={[
{
id: '7',
type: 'folder',
name:
'Pictures'
},
{
id: '8',
type: 'file',
name:
'Adobe Fresco'
},
{
id: '9',
type: 'folder',
name: 'Apps'
},
{
id: '10',
type: 'file',
name:
'Adobe Illustrator'
},
{
id: '11',
type: 'file',
name:
'Adobe Lightroom'
},
{
id: '12',
type: 'file',
name:
'Adobe Dreamweaver'
}
]}
aria-label="Second ListBox"
/>
</div>
Advanced customization#
Composition#
If you need to customize one of the components within a ListBox
, such as Item
or Section
, in many cases you can create a wrapper component. This lets you customize the props passed to the component.
function MyItem(props) {
return <Item {...props} className="my-item" />
}
function MyItem(props) {
return <Item {...props} className="my-item" />
}
function MyItem(props) {
return (
<Item
{...props}
className="my-item"
/>
);
}
Hooks#
If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useListBox for more details.