React Aria collection components support drag and drop with mouse and touch interactions, and full keyboard and screen reader accessibility. Learn how to provide drag data and handle drop events to move, insert, or reorder items.
Introduction
Drag and drop allows a user to move data between two locations. The initial location is referred to as a drag source, and the final location is referred to as a drop target. The dragged data consists of one or more drag items, each of which contains data such as text, files, or application-specific objects. These are shown in a drag preview under the user's cursor.
Drag and drop is implemented using the useDragAndDrop hook. The result of this function is passed into components that support drag and drop, such as ListBox, GridList, Tree, and Table.
Drag source
A drag source provides one or more drag items. Each item includes one or more data formats, e.g. JSON, HTML, or plain text. Drag types can be standard mime types or application-specific strings. Providing multiple data formats allows the user to drop in external applications such as email clients or text editors.
This example provides items as plain text, HTML, and a custom app-specific data format. Dropping within this page will use the custom data format. 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.
import {useDragAndDrop} from 'react-aria-components';
import {PokemonListBox, Pokemon} from './PokemonListBox';
import {DroppableListBox} from './DroppableListBox';
function DraggableListBox() {
let {dragAndDropHooks} = useDragAndDrop<Pokemon>({
getItems(keys, values) {
return values.map(item => {
return {
'text/plain': `${item.name} – ${item.type}`,
'text/html': `<strong>${item.name}</strong> – <em>${item.type}</em>`,
'pokemon': JSON.stringify(item)
};
});
}
});
return <PokemonListBox dragAndDropHooks={dragAndDropHooks} />;
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DraggableListBox />
<DroppableListBox />
</div>
Drag preview
While dragging, a drag preview is displayed under the user's mouse or finger to represent the items being dragged. By default, this is a copy of the dragged element. A custom preview can be rendered by the renderDragPreview function, which receives the dragged data, and returns a JSX element.
This example renders a custom drag preview which shows the number of items being dragged.
import {useDragAndDrop} from 'react-aria-components';
import {PokemonListBox, Pokemon} from './PokemonListBox';
function DraggableListBox() {
let {dragAndDropHooks} = useDragAndDrop({
renderDragPreview(items) {
return (
<div className="drag-preview">
{items[0]['text/plain']}
<span className="badge">{items.length}</span>
</div>
);
}
});
return <PokemonListBox dragAndDropHooks={dragAndDropHooks} />;
}
Drop items
Users can drop one or more drop 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.
This example uses the acceptedDragTypes prop to accept items that include an app-specific type, which is retrieved using the item's getText method. When acceptedDragTypes is specified, the dropped items are filtered to include only items that include the accepted types.
import {isTextDropItem, useDragAndDrop} from 'react-aria-components';
import {useState} from 'react';
import {PokemonListBox, Pokemon} from './PokemonListBox';
import {DraggableListBox} from './DraggableListBox';
function DroppableListBox() {
let [items, setItems] = useState<Pokemon[]>([]);
let {dragAndDropHooks} = useDragAndDrop<Pokemon>({
acceptedDragTypes: ['pokemon'],
async onRootDrop(e) {
let items = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async item => JSON.parse(await item.getText('pokemon')))
);
setItems(items);
}
});
return <PokemonListBox items={items} dragAndDropHooks={dragAndDropHooks} />;
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<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 File object for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local object URL.
import {GridList, GridListItem} from './GridList';
import {useDragAndDrop, isFileDropItem, Text} from 'react-aria-components';
import {useState} from 'react';
interface ImageItem {
id: number,
url: string,
name: string
}
function DroppableGridList() {
let [items, setItems] = 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 (
<GridList
aria-label="Droppable list"
items={items}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => "Drop images here"}
style={{height: 250}}
data-size="small">
{item => (
<GridListItem textValue={item.name}>
<img src={item.url} />
<Text>{item.name}</Text>
</GridListItem>
)}
</GridList>
);
}
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. Include the special DIRECTORY_DRAG_TYPE type in acceptedDragTypes to limit drops to directories.
import {Tree, TreeItem} from './Tree';
import {useDragAndDrop, DIRECTORY_DRAG_TYPE, type DirectoryDropItem, isDirectoryDropItem, Collection} from 'react-aria-components';
import {useState} from 'react';
import File from '@react-spectrum/s2/icons/File';
import Folder from '@react-spectrum/s2/icons/Folder';
interface DirItem {
id: number,
name: string,
kind: string,
children: DirItem[]
}
function DroppableTree() {
let [files, setFiles] = useState<DirItem[]>([]);
let {dragAndDropHooks} = useDragAndDrop({
acceptedDragTypes: [DIRECTORY_DRAG_TYPE],
async onRootDrop(e) {
// Read entries in directory and update state with relevant info.
let getFiles = async (dir: DirectoryDropItem): Promise<DirItem[]> => {
let files: DirItem[] = [];
for await (let entry of dir.getEntries()) {
files.push({
id: Math.random(),
name: entry.name,
kind: entry.kind,
children: entry.kind === 'directory' ? await getFiles(entry) : []
});
}
return files;
};
let dir = e.items.find(isDirectoryDropItem)!;
setFiles(await getFiles(dir));
}
});
return (
<Tree
aria-label="Droppable tree"
items={files}
dragAndDropHooks={dragAndDropHooks}
renderEmptyState={() => 'Drop directory here'}
style={{height: 250}}>
{function renderItem(item) {
return (
<TreeItem
title={
<div style={{display: 'flex', alignItems: 'center', gap: 8}}>
<span style={{flex: '0 0 auto'}}>{item.kind === 'directory' ? <Folder /> : <File />}</span>
<span style={{flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'hidden'}}>{item.name}</span>
</div>
}>
<Collection items={item.children}>
{renderItem}
</Collection>
</TreeItem>
);
}}
</Tree>
);
}
Drop positions
Collection components such as ListBox, Table, Tree, and GridList support multiple drop positions.
- The
"root"drop position allows dropping on the collection as a whole. - The
"on"drop position allows dropping on individual collection items, such as a folder within a list. - The
"before"and"after"drop positions allow the user to insert or move items between other items. This is displayed by rendering a drop indicator between items.
Dropping on the collection
Use the onRootDrop event to enable dropping on the entire collection. When a valid drag hovers over the collection, it receives the data-drop-target state.
import {isTextDropItem, useDragAndDrop} from 'react-aria-components';
import {useState} from 'react';
import {PokemonListBox, Pokemon} from './PokemonListBox';
import {DraggableListBox} from './DraggableListBox';
function DroppableListBox() {
let [items, setItems] = useState<Pokemon[]>([]);
let {dragAndDropHooks} = useDragAndDrop<Pokemon>({
acceptedDragTypes: ['pokemon'],
async onRootDrop(e) {
let items = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async item => JSON.parse(await item.getText('pokemon')))
);
setItems(items);
}
});
return <PokemonListBox items={items} dragAndDropHooks={dragAndDropHooks} />;
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DraggableListBox />
<DroppableListBox />
</div>
Dropping on items
Use the onItemDrop event to enable dropping on items. When a valid drag hovers over an item, it receives the data-drop-target state.
import {useDragAndDrop} from 'react-aria-components';
import {PokemonListBox} from './PokemonListBox';
import {DraggableListBox} from './DraggableListBox';
function DroppableListBox() {
let {dragAndDropHooks} = useDragAndDrop({
onItemDrop(e) {
alert(`Dropped on ${e.target.key}`);
}
});
return (
<PokemonListBox
dragAndDropHooks={dragAndDropHooks}
items={[
{id: 1, name: 'Beedrill', type: 'Bug, Poison', level: 25},
{id: 2, name: 'Pidgeot', type: 'Flying', level: 40},
{id: 3, name: 'Fearow', type: 'Flying', level: 32},
{id: 4, name: 'Jigglypuff', type: 'Fairy', level: 56}
]} />
);
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DraggableListBox />
<DroppableListBox />
</div>
Dropping between items
Use the onInsert event to enable dropping between items. React Aria renders a DropIndicator between items to indicate the insertion position, which can be customized using renderDropIndicator.
import {useDragAndDrop, isTextDropItem, DropIndicator, useListData} from 'react-aria-components';
import {PokemonListBox} from './PokemonListBox';
import {DraggableListBox} from './DraggableListBox';
function DroppableListBox() {
let list = useListData({
initialItems: [
{id: 1, name: 'Beedrill', type: 'Bug, Poison', level: 25},
{id: 2, name: 'Pidgeot', type: 'Flying', level: 40},
{id: 3, name: 'Fearow', type: 'Flying', level: 32},
{id: 4, name: 'Jigglypuff', type: 'Fairy', level: 56}
]
});
let {dragAndDropHooks} = useDragAndDrop({
async onInsert(e) {
let items = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async item => {
let pokemon = JSON.parse(await item.getText('pokemon'));
let processItem = item => ({
...item,
id: Math.random(),
children: item.children?.map(processItem)
});
return processItem(pokemon);
})
);
if (e.target.dropPosition === 'before') {
list.insertBefore(e.target.key, ...items);
} else if (e.target.dropPosition === 'after') {
list.insertAfter(e.target.key, ...items);
}
},
renderDropIndicator(target) {
return <DropIndicator target={target} />;
}
});
return <PokemonListBox items={list.items} dragAndDropHooks={dragAndDropHooks} />;
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DraggableListBox />
<DroppableListBox />
</div>
Reordering items
Use the onReorder event to enable reordering items. For components with hierarchy like Tree, this only allows reordering within the same level. Use onMove to allow moving items between levels.
import {useDragAndDrop, useListData} from 'react-aria-components';
import {PokemonListBox, Pokemon, defaultItems} from './PokemonListBox';
function ReorderableListBox() {
let list = useListData({
initialItems: defaultItems
});
let {dragAndDropHooks} = useDragAndDrop({
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 <PokemonListBox items={list.items} dragAndDropHooks={dragAndDropHooks} />
}
Moving items
Use the onMove event to enable moving items within a collection. This allows reordering items within a level and moving items between levels. It supports dropping both on and between items.
import {useDragAndDrop, useTreeData} from 'react-aria-components';
import {PokemonListBox, Pokemon, defaultItems} from './PokemonListBox';
function ReorderableListBox() {
let tree = useTreeData({
initialItems: defaultItems
});
let {dragAndDropHooks} = useDragAndDrop({
onMove(e) {
if (e.target.dropPosition === 'before') {
tree.moveBefore(e.target.key, e.keys);
} else if (e.target.dropPosition === 'after') {
tree.moveAfter(e.target.key, e.keys);
} else if (e.target.dropPosition === 'on') {
// Move items to become children of the target
let targetNode = tree.getItem(e.target.key);
if (targetNode) {
let targetIndex = targetNode.children
? targetNode.children.length
: 0;
let keyArray = Array.from(e.keys);
for (let i = 0; i < keyArray.length; i++) {
tree.move(keyArray[i], e.target.key, targetIndex + i);
}
}
}
}
});
// Map tree items to Pokemon objects
let processItem = item => {
return {...item.value, children: item.children.map(processItem)}
};
let items = tree.items.map(processItem);
return <PokemonListBox items={items} dragAndDropHooks={dragAndDropHooks} />
}
Multiple positions
This example puts together many of the examples 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.
import {useDragAndDrop, isTextDropItem, useListData} from 'react-aria-components';
import {PokemonListBox, Pokemon, defaultItems} from './PokemonListBox';
interface DndListBoxProps {
initialItems: Pokemon[],
'aria-label': string
}
function DndListBox(props: DndListBoxProps) {
let list = useListData({
initialItems: props.initialItems
});
let {dragAndDropHooks} = useDragAndDrop({
renderDragPreview(items) {
return (
<div className="drag-preview">
{items[0]['text/plain']}
<span className="badge">{items.length}</span>
</div>
);
},
// Provide drag data in a custom format as well as plain text.
getItems(keys, items: Pokemon[]) {
return items.map(item => ({
'pokemon': JSON.stringify(item),
'text/plain': item.name
}));
},
// Accept drops with the custom format.
acceptedDragTypes: ['pokemon'],
// 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('pokemon')))
);
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('pokemon')))
);
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 <PokemonListBox items={list.items} dragAndDropHooks={dragAndDropHooks} />
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DndListBox
initialItems={defaultItems}
aria-label="Drag and drop ListBox" />
<DndListBox
initialItems={[]}
aria-label="Drag and drop ListBox" />
</div>
Drop operations
A DropOperation is an indication of what will happen when dragged data is dropped on a particular drop target. These are:
move– the dragged data will be moved from its source location to the target location.copy– the dragged data will be copied to the target destination.link– a relationship will be established between the source and target locations.cancel– 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.
getDropOperation
Use getDropOperation option to provide 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. It should return the operation that will be performed on drop, or 'cancel' to reject the drop. If the returned operation is not in allowedOperations, the drop will be canceled.
import {isTextDropItem, useDragAndDrop} from 'react-aria-components';
import {useState} from 'react';
import {PokemonListBox, Pokemon} from './PokemonListBox';
import {DraggableListBox} from './DraggableListBox';
function DroppableListBox() {
let [items, setItems] = useState<Pokemon[]>([]);
let {dragAndDropHooks} = useDragAndDrop<Pokemon>({
acceptedDragTypes: ['pokemon'],
getDropOperation: (target, types, allowedOperations) => 'copy',
async onRootDrop(e) {
let items = await Promise.all(
e.items
.filter(isTextDropItem)
.map(async item => JSON.parse(await item.getText('pokemon')))
);
setItems(items);
}
});
return <PokemonListBox items={items} dragAndDropHooks={dragAndDropHooks} />;
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DraggableListBox />
<DroppableListBox />
</div>
getAllowedDropOperations
The drag source can also control which drop operations are allowed. In the example below, the cursor shows the copy cursor by default, and pressing a modifier key cancels the drop.
import {useDragAndDrop, useListData} from 'react-aria-components';
import {PokemonListBox, Pokemon, defaultItems} from './PokemonListBox';
import {DroppableListBox} from './DroppableListBox';
function DraggableListBox() {
let {dragAndDropHooks} = useDragAndDrop<Pokemon>({
getAllowedDropOperations: () => ['copy']
});
return <PokemonListBox items={list.items} dragAndDropHooks={dragAndDropHooks} />
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DraggableListBox />
<DroppableListBox />
</div>
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.
import {useDragAndDrop, useListData} from 'react-aria-components';
import {PokemonListBox, Pokemon, defaultItems} from './PokemonListBox';
import {DroppableListBox} from './DroppableListBox';
function DraggableListBox() {
let {dragAndDropHooks} = useDragAndDrop<Pokemon>({
onDragEnd(e) {
if (e.dropOperation === 'move') {
list.remove(...e.keys);
}
}
});
return <PokemonListBox items={list.items} dragAndDropHooks={dragAndDropHooks} />
}
<div style={{display: 'flex', gap: 12, flexWrap: 'wrap', width: '100%', justifyContent: 'center'}}>
<DraggableListBox />
<DroppableListBox />
</div>
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;
}
};
Accessibility
While drag and drop has historically been mostly limited to mouse and touchscreen users, keyboard and screen reader friendly alternatives are important for users who cannot use these interaction methods. React Aria implements keyboard and screen reader interactions for drag and drop that provide full parity with the mouse and touch experiences.
Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to cycle between the drop targets that accept the dragged data, and Enter to drop. Escape cancels a drag. Touch screen reader users can also drag by double tapping to activate drag and drop mode, swiping between drop targets, and double tapping again to drop. Screen reader announcements are included to help guide the user through this process.
Collection components such as GridList and Table are treated as a single drop target, so that users can easily tab past them 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.
Draggable elements can sometimes have conflicting keyboard interactions, such as selection. These are handled by adding an explicit drag affordance. Keyboard and screen reader users can focus this element, and use it to initiate drag and drop for the parent item. In addition, this has the added benefit of making drag and drop more discoverable.
Note that because mouse and touch drag and drop interactions utilize the native browser APIs, they work both within the browser window and with external applications on the user's device. Keyboard and screen reader drag and drop is implemented from scratch, and therefore can only be supported within the browser window. Alternative interactions for operations involving external applications, such as file uploading or copy and paste, should be implemented in addition to drag and drop.
Examples
API
useDragAndDrop
| Name | Type | |
|---|---|---|
isDisabled | boolean | |
| Whether the drag and drop events should be disabled. | ||
getItems | | |
| A function that returns the items being dragged. If not specified, we assume that the collection is not draggable. | ||
renderDragPreview | | |
| A function that renders a drag preview, which is shown under the user's cursor while dragging. By default, a copy of the dragged element is rendered. | ||
getAllowedDropOperations | | |
| Function that returns the drop operations that are allowed for the dragged items. If not provided, all drop operations are allowed. | ||
onDragStart | | |
| Handler that is called when a drag operation is started. | ||
onDragMove | | |
| Handler that is called when the drag is moved. | ||
onDragEnd | | |
| Handler that is called when the drag operation is ended, either as a result of a drop or a cancellation. | ||
onRootDrop | | |
| Handler that is called when external items are dropped on the droppable collection's root. | ||
onItemDrop | | |
| Handler that is called when items are dropped "on" an item. | ||
onInsert | | |
| Handler that is called when external items are dropped "between" items. | ||
onReorder | | |
| Handler that is called when items are reordered within the collection. This handler only allows dropping between items, not on items. It does not allow moving items to a different parent item within a tree. | ||
onMove | | |
| Handler that is called when items are moved within the source collection. This handler allows dropping both on or between items, and items may be moved to a different parent item within a tree. | ||
onDropEnter | | |
| Handler that is called when a valid drag enters a drop target. | ||
onDropExit | | |
| Handler that is called when a valid drag exits a drop target. | ||
onDropActivate | | |
| Handler that is called after a valid drag is held over a drop target for a period of time. | ||
onDrop | | |
Handler that is called when a valid drag is dropped on a drop target. When defined, this overrides other
drop handlers such as onInsert, and onItemDrop. | ||
renderDropIndicator | | |
A function that renders a drop indicator element between two items in a collection.
This should render a <DropIndicator> element. If this function is not provided, a
default DropIndicator is provided. | ||
acceptedDragTypes | 'all' | Array | |
The drag types that the droppable collection accepts. If the collection accepts directories, include DIRECTORY_DRAG_TYPE in your array of allowed types. | ||
shouldAcceptItemDrop | | |
| A function returning whether a given target in the droppable collection is a valid "on" drop target for the current drag types. | ||
getDropOperation | (
target: DropTarget,
types: DragTypes,
allowedOperations: DropOperation | |
| A function returning the drop operation to be performed when items matching the given types are dropped on the drop target. | ||
dropTargetDelegate | DropTargetDelegate | |
| A custom delegate object that provides drop targets for pointer coordinates within the collection. | ||
DropIndicator
| Name | Type | |
|---|---|---|
target | DropTarget | |
| The drop target that the drop indicator represents. | ||
children | ChildrenOrFunction | |
| The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-DropIndicator
| Render Prop | CSS Selector |
|---|---|
isDropTarget | CSS Selector: [data-drop-target]
|
| Whether the drop indicator is currently the active drop target. | |