This RFC describes the interactions and APIs React Spectrum and React Aria will implement in order to support drag and drop between various components. It defines the interactions that will be implemented to allow both traditional drag and drop with a mouse or touch screen, and also keyboard and screen reader interactions for accessibility.
Many components in React Spectrum display various types of data, which users may need to move or copy between locations in an application. Drag and drop is an efficient, intuitive way of performing these operations that is widely supported across both desktop and mobile operating systems. Users expect to be able to drag items in lists, tables, trees, and grids between locations, or upload files from their desktop via drag and drop. In order to implement drag and drop in React Spectrum, we need to define the interactions and behaviors that will be supported across all input methods.
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 source and target may be within the same application, for example two adjacent lists, or in different applications, for example uploading files from the desktop.
The drag source is responsible for providing one or more drag items, which specify the data to be dragged. This may be an object represented by the draggable element, a file, plain text content, etc. Each drag item is represented as a standard mime type and the actual data for the item. Multiple representations of the same object may be provided in different formats for interoperability with various drop targets. This allows drag and drop to work even between different applications. For example, a drag source may provide the data for a drag in both a custom object format and as plain text or a standard image format to allow dropping into both the application itself for richer functionality, and also destinations that don’t understand the application specific format.
While the user is performing a drag, the application provides feedback to indicate what elements accept drops for the items the user is dragging. For example, when dragging with a mouse, drop targets often display a highlighted visual state when a drag hovers over them to indicate that the user can drop there. If the drop target does not accept the type of data being dragged, it should not be highlighted, and attempting to drop over the target should result in the drag being canceled.
In order to move content from one location to another, a user may need to trigger a navigation while the drag is in progress. For example, they may need to drag content to a drop target that is not currently visible, e.g. in a different section of the application. However, they cannot typically perform the navigation via the usual gesture (e.g. clicking) because their pointer is otherwise occupied with the drag. In order to solve this, many applications allow users to activate navigational controls (e.g. buttons, tabs, breadcrumbs, tree items, etc.) by dragging over them and pausing briefly. For example, in Finder on macOS, users can drag a file over a folder and hold for a second to navigate into that folder. This allows them to drag and drop into a different location without needing two windows. This is sometimes referred to as “spring loading”, but henceforth we will refer to this as drag activation.
A drop target may accept drops over its entire area, or it may support multiple drop positions within it. For example, collection components like lists, trees, and tables may allow a user to drop on the whole collection, or on individual items within the collection (e.g. folders). If the collection allows the user to control the order, it may also allow inserting items between other items, or reordering items by drag and drop. This is often shown by rendering a drop indicator between two items when the user drags over the space between them. A collection may only allow dropping on certain items (e.g. only on folders and not files), and both dropping on and between items can be supported simultaneously.
When a drop is performed, the drag items are transferred from the drag source to the drop target. Depending on the context of the drag, the items may be moved, copied, or linked/referenced. This is referred to as a drop operation. The drag source and drop target negotiate what drop operation will be performed. The drag source can specify what drop operations are allowed for the data it provides, and the drop target can select one of these operations to perform. In addition, the user can often perform an interaction while dragging to influence the operation as well. For example, on macOS pressing the Option key changes the drop operation from move to copy.
Once a drop is completed, the dragged items typically appear inside the drop target to indicate that they have been dropped. When a drop results in data being moved, the items also disappear from the drag source. In order for the drag source and drop targets to be unaware of each other, or in different applications, the drop operation that was performed is communicated back to the drag source by the system so that it can update itself accordingly.
With a mouse, a user can click an item and drag by holding the mouse button down and moving the pointer. On a touch screen the interaction is similar, but often starts after a long press in order to distinguish from scroll or swipe gestures.
Once dragging, a drag preview is shown under the user’s cursor/finger as they move around. This is typically a smaller size element from the original dragged element so that it does not cover too much of the drop target, allowing the user to see where they will drop. When multiple elements are dragged at once, they are often displayed as a stack, with a badge showing the count of the total number of items.
When a drag hovers over a drop target that accepts the data being dragged, it is highlighted to give the user feedback that they can drop the items on that target. The drop operation that will be performed is typically indicated to the user in the form of a cursor when hovering over a drop target. For example, a green plus sign is shown next to the cursor when the drop will result in a copy. The user can often change the operation that will be performed by pressing a modified key, such as Option or Command to change the drop operation to copy or move, respectively.
To cancel a drag once it has started, the user can either press the Escape key on their keyboard, or drop over an area that doesn’t accept the drop. When a drop is canceled, the drag preview often slides back to the original drag source to indicate where it came from. When dropping over a valid drop target, the drag preview disappears and the drop operation is performed. This usually results in the dropped items appearing inside the drop target to indicate that they have been successfully dropped.
While drag and drop has historically been mostly limited to mouse and touchscreen users, keyboard alternatives are important for users who cannot use these interaction methods. For example, copy and paste can often be used as an alternative to drag and drop to allow the user to move an object from one location to another. However, copy and paste does not cover all of the possible interactions that drag and drop allows. For example, it’s hard to specify an exact location to insert an item into a list on paste, or even know where pasting is accepted. Users must either already know where they can paste, or navigate aimlessly until they find a valid location.
We believe we can improve on this to provide a keyboard experience that gives the full power of drag and drop to keyboard users. This can be accomplished by mimicking how drag and drop works for mouse and touchscreen users, but using only keyboard commands. The general idea is to enter a drag and drop mode by pressing Enter on the drag source, navigating between targets using the keyboard, and dropping by pressing Enter again. When in drag and drop mode, only drop targets that accept the drop can be navigated to, and they show the same drop highlighted appearance when focused. This makes it much easier for the user to find a valid location to drop, and get access to all of the features that mouse and touchscreen users have.
To activate dragging, we propose adding a drag affordance to draggable elements, such as a drag handle icon. This should be a button, that when pressed using the Enter or Space keys, triggers drag and drop for the associated draggable element. This affordance is necessary in many cases where pressing Enter or Space over the draggable element itself already has an action, such as item selection in a list or table. An alternative keyboard command could be invented for this purpose, but an affordance is more clear, and an explicit button for screen reader users would be needed in any case.
Once the user presses Enter on a drag affordance, the application enters drag and drop mode. In this mode, the Tab key can be used to navigate between valid drop targets. All other interactions are disabled while in drag and drop mode, including all buttons, navigation controls, etc. The only elements that can be accessed are the drop targets that support the type of data being dragged. Any drop targets that do not support the dragged items are skipped. The usual tab order is replaced by the sequence of drop targets, followed by the original button that triggered drag and drop mode. Pressing Escape or triggering the original drag affordance again cancels dragging and exits drag and drop mode. Pressing Enter while over a drop target triggers dropping.
While the Tab key is used to navigate between drop targets, a drop target may allow dropping on or between multiple elements within it. For example, in a list or table, the user may be able to drop on elements such as folders, or between elements to insert at a particular drop position. Collection elements such as these are typically represented by a single Tab stop, with arrow key navigation internally. In drag and drop mode, this is no different. The user can tab to the collection, and then use their arrow keys to navigate to the position within the collection where they wish to drop. Only items that accept the dragged data are navigated to, and others are skipped. If dropping between items is supported, drop indicators are inserted between each item and are also navigated to using the arrow keys.
There are two open questions for keyboard drag and drop support: how to trigger drag activation over a target, and how to allow the user to select a drop operation to perform. Mimicking dragging and holding over a target to activate it with a keyboard wouldn’t really be possible, so either an alternate keyboard command or some kind of menu of options when pressing Enter will be needed. Selecting a drop operation could be done by mimicking the modifier keys (e.g. Option/Command) used to do so with a mouse, but this may be undiscoverable. A menu may also work in this case.
Once the user presses Enter over a valid drop target, or cancels the drag by pressing Escape, drag and drop mode is exited and normal user interaction resumes. The data is transferred to the drop target, and the UI updates accordingly. Keyboard focus may also be moved to the newly inserted elements, if any.
Screen reader users also need to be able to perform drag and drop tasks. While many screen reader users can use the keyboard interactions described above, touch screen reader users may not. Touch screen readers navigate an application by swiping or tapping to move the virtual cursor, and can use double tap gestures to click.
The general pattern for screen readers follows the same interactions as for keyboard. A drag affordance can be clicked with the screen reader cursor to enter drag and drop mode, the user can navigate to a valid drop target, and drop by clicking the target. Drag and drop mode works similarly to the keyboard, where only valid drop targets can be navigated to. All other elements in the application are hidden from the screen reader using aria-hidden
. This makes it easier for a screen reader user to find the valid drop targets.
Drag sources and drop targets should be carefully labeled to provide the user with instructions on how to perform a drag. Drag affordances should include a description to specify that the user should activate the button to start/stop dragging. Drop targets should include a description to specify how to drop. Both of these should be contextual based on the current interaction modality. For example, if the user is using a keyboard, specify that they should press Enter to drag/drop, and if they are on a touch device, specify that they should double tap.
Insertion indicators in a list should include a label to specify what items they are between. While they may need to have a contextual role based on their parent for correct ARIA (e.g. in a listbox, children must be options), aria-roledescription
can be used to specify a custom description to be used instead since the indicator isn’t really an item in the list.
Finally, a live region can be used to specify information and instructions during a drag. When a drag begins, an announcement that drag mode has been entered with instructions for how to navigate to a target and perform a drop should be announced. This can also be contextual based on the interaction modality. When a drop is canceled or completed, this should also be announced.


A prototype of the screen reader interactions for dragging items between lists, shown here using an iPad. Click to see the video with audio.
The open questions for screen reader drag and drop interactions are the same as for keyboard: how to activate an item while dragging to trigger navigation as opposed to dropping, and how to allow the user to select a drop operation. For screen reader users without a keyboard (e.g. touch devices), this is more challenging, since only a single interaction (clicking) can be performed. Thus, a menu of possibilities that appears on click may be the way forward.
One limitation of implementing keyboard and screen reader support for drag and drop ourselves rather than relying on the operating system to do so is that accessible drag and drop between applications cannot be achieved. This means that file uploads and other cross application drags cannot be supported using the same accessible patterns presented above. This also affects things inside applications such as cross domain drag and drop between iframes. One way to support these use cases would be to also support copy and paste in addition to drag and drop. This does work across applications since it’s managed by the system, but has the limitations mentioned previously regarding selecting a location to drop and knowing where the valid drop targets are. However, this may be good enough for simple use cases like uploading files.
The following are the proposed APIs for the drag and drop hooks to be implemented in React Aria. They are designed such that components using them can adopt a single API and get support for all types of interactions at once.
The useDrag hook makes an element draggable and provides the data to drag. It returns props for the element to drag, and also to a drag affordance button that triggers keyboard/screen reader accessible dragging mode. It also returns whether the element is currently being dragged, which allows the component to update its visual appearance while dragging (e.g. dim it).
interface DragItem {
/** A list of mime types or custom drag types that this item provides. */
types: Iterable<string>,
/** A function that the item data for a given drag type. */
getData: (type: string) => string
}
interface DragDropEvent {
// Relative to the target element's position
x: number,
y: number
}
interface DragStartEvent extends DragDropEvent {
type: 'dragstart'
}
interface DragMoveEvent extends DragDropEvent {
type: 'dragmove'
}
type DropOperation = 'copy' | 'link' | 'move' | 'cancel';
interface DragEndEvent extends DragDropEvent {
type: 'dragend',
/**
* The operation that was performed by the drop target.
* Allows the drag target to respond accordingly, e.g. by removing the
* dragged items if they were moved.
*/
dropOperation: DropOperation
}
interface DragOptions {
/** Fired when a drag begins. */
onDragStart?: (e: DragStartEvent) => void,
/** Fired when the position of the drag moves. */
onDragMove?: (e: DragMoveEvent) => void,
/** Fired when the drag ends, either as a result of a drop or a cancelation. */
onDragEnd?: (e: DragEndEvent) => void,
/** Called when a drag starts to get the items to drag. */
getItems: () => DragItem[],
/** Function that is called to render a preview to display while dragging. */
renderPreview?: (items: DragItem[]) => JSX.Element,
/**
* Optional function to return the drop operations that are allowed for
* the dragged items. If not provided, all drop operations are allowed.
*/
getAllowedDropOperations?: () => DropOperation[]
}
interface DragResult {
/** Props for the draggable element. */
dragProps: HTMLAttributes<HTMLElement>,
/** Props for the drag affordance button. */
dragButtonProps: ButtonHTMLAttributes<HTMLButtonElement>,
/** Whether the element is currently being dragged. */
isDragging: boolean
}
declare function useDrag(options: DragOptions): DragResult;
The useDrop hook makes an element a drop target and handles drop events. It returns props for the drop target element, and also returns whether that element is currently an active drop target (for visual state changes).
interface DropEnterEvent extends DragDropEvent {
type: 'dropenter'
}
interface DropMoveEvent extends DragDropEvent {
type: 'dropmove'
}
interface DropActivateEvent extends DragDropEvent {
type: 'dropactivate'
}
interface DropExitEvent extends DragDropEvent {
type: 'dropexit'
}
interface DropItem {
/** The list of drag types that are available for this item. */
types: Set<string>,
/** Retrieves the item data for a given drag type */
getData(type: string): Promise<string>
}
interface DropEvent extends DragDropEvent {
type: 'drop',
dropOperation: DropOperation,
items: DropItem[]
}
interface DropOptions {
/** A ref to the drop target element. */
ref: RefObject<HTMLElement>,
/**
* A function that returns the drop operation that will be performed
* if items of the given types are dropped. A list of operations that
* are allowed by the drag source are provided. If an operation is
* returned that is not allowed, the drag will be canceled.
*/
getDropOperation?: (types: string[], allowedOperations: DropOperation[]) => DropOperation,
/**
* Similar to getDropOperation, this function should be provided if the drop
* operation can vary depending on the point within it that the drag is over.
*/
getDropOperationForPoint?: (types: string[], allowedOperations: DropOperation[], x: number, y: number) => DropOperation,
/** Fired when a valid drag enters the drop target. */
onDropEnter?: (e: DropEnterEvent) => void,
/** Fired when a drag moves while over the drop target. */
onDropMove?: (e: DropMoveEvent) => void,
/**
* Fired when the user hovers over the drop target for a period of time.
* Typically, this opens or navigates to that item.
*/
onDropActivate?: (e: DropActivateEvent) => void,
/** Fired when a drag exits the drop target. */
onDropExit?: (e: DropExitEvent) => void,
/** Fired when a drop occurs on the drop target. */
onDrop?: (e: DropEvent) => void
}
interface DropResult {
/** Props for the drop target element. */
dropProps: HTMLAttributes<HTMLElement>,
/** Whether the element is currently an active drop target. */
isDropTarget: boolean
}
declare function useDrop(options: DropOptions): DropResult;
The useDraggableCollectionState hook handles state for dragging from a collection component, including integrating with the selection system to handle dragging multiple items.
interface DraggableCollectionOptions {
/** The collection. */
collection: Collection<Node<unknown>>,
/** The selection manager for the collection. */
selectionManager: MultipleSelectionManager
/** Called when a drag starts in the collection. */
onDragStart?: (e: DraggableCollectionStartEvent) => void,
/** Called when a drag that started in the collection moves. */
onDragMove?: (e: DraggableCollectionMoveEvent) => void,
/** Called when a drag that started in the collection ends. */
onDragEnd?: (e: DraggableCollectionEndEvent) => void,
/** Returns items to be dragged for the given keys in the collection. */
getItems: (keys: Set<Key>) => DragItem[],
/** Renders a preview to show for the given keys. `draggedKey` represents the key of the item the user actually dragged. */
renderPreview?: (keys: Set<Key>, draggedKey: Key) => JSX.Element,
/**
* Optional function to return the drop operations that are allowed for
* the dragged items. If not provided, all drop operations are allowed.
*/
getAllowedDropOperations?: () => DropOperation[]
}
interface DraggableCollectionState {
/** The collection. */
collection: Collection<Node<unknown>>,
/** The selection manager for the collection. */
selectionManager: MultipleSelectionManager,
/** Whether the given key is currently being dragged. */
isDragging(key: Key): boolean,
/** Returns the drag items to drag when starting a drag from the given key. */
getItems(key: Key): DragItem[],
/** Renders a preview to show when starting a drag from the given key. */
renderPreview(key: Key): JSX.Element,
/** Starts a drag from the given key. Fires the onDragStart event. */
startDrag(key: Key, event: DragStartEvent): void,
/** Fires the onDragMove event. */
moveDrag(event: DragMoveEvent): void,
/** Cancels the current drag, and fires the onDragEnd event. */
endDrag(event: DragEndEvent): void
}
declare function useDraggableCollectionState(props: DraggableCollectionOptions): DraggableCollectionState;
The useDraggableItem hook works together with useDraggableCollectionState to handle drags on individual items within a collection.
interface DraggableItemProps {
/** The key of the item. */
key: Key
}
interface DraggableItemResult {
/** Props for the draggable item. */
dragProps: HTMLAttributes<HTMLElement>,
/** Props for the button within the draggable item that triggers keyboard dragging. */
dragButtonProps: AriaButtonProps
}
declare function useDraggableItem(props: DraggableItemProps, state: DraggableCollectionState): DraggableItemResult;
The useDroppableCollection hook handles drops for collection components, which allow drops on or between multiple elements within them. Drop targets within the collection are represented as an item key and a drop position, which can be “on”, “before”, or “after”. Event objects are the same as for useDrop, but have an additional target
field to specify which target the drop is over.
/** A drop target that represents dropping on the whole collection. */
interface RootDropTarget {
type: 'root'
}
type DropPosition = 'on' | 'before' | 'after';
interface ItemDropTarget {
type: 'item',
key: Key,
dropPosition: DropPosition
}
type DropTarget = RootDropTarget | ItemDropTarget;
interface DroppableCollectionEnterEvent extends DropEnterEvent {
target: DropTarget
}
interface DroppableCollectionMoveEvent extends DropMoveEvent {
target: DropTarget
}
interface DroppableCollectionActivateEvent extends DropActivateEvent {
target: DropTarget
}
interface DroppableCollectionExitEvent extends DropExitEvent {
target: DropTarget
}
interface DroppableCollectionDropEvent extends DropEvent {
target: DropTarget
}
interface DroppableCollectionOptions {
/** A function that returns the allowed drop positions for a given key. */
getAllowedDropPositions?: (key: Key) => DropPosition[],
/**
* A function that returns the drop operation that will be performed
* when dropping items of the given type on the specified target.
*/
getDropOperation?: (target: DropTarget, types: string[], allowedOperations: DropOperation[]) => DropOperation,
/** Fired when a valid drag enters a drop target within the collection. */
onDropEnter?: (e: DroppableCollectionEnterEvent) => void,
/** Fired when a drag moves while over a drop target within the collection. */
onDropMove?: (e: DroppableCollectionMoveEvent) => void,
/**
* Fired when the user hovers over a drop target within the collection
* for a period of time. Typically, this opens or navigates to that item.
*/
onDropActivate?: (e: DroppableCollectionActivateEvent) => void,
/** Fired when a drag exits a drop target within the collection. */
onDropExit?: (e: DroppableCollectionExitEvent) => void,
/** Fired when a drop occurs on a drop target within the collection. */
onDrop?: (e: DroppableCollectionDropEvent) => void
}
interface DroppableCollectionStateOptions extends DroppableCollectionOptions {
collection: Collection<Node<unknown>>
}
interface DroppableCollectionState {
collection: Collection<Node<unknown>>,
/** The current drop target within the collection. */
target: DropTarget,
/** Sets the current drop target. Fires onDragExit and onDragEnter as needed. */
setTarget(target: DropTarget): void,
/** Returns whether the given target is the current drop target within the collection. */
isDropTarget(target: DropTarget): boolean,
/** Returns the drop operation to perform when dropping on the given target. */
getDropOperation(target: DropTarget, types: Set<string>, allowedOperations: DropOperation[]): DropOperation
}
declare function useDroppableCollectionState(options: DroppableCollectionStateOptions): DroppableCollectionState;
interface AriaDroppableCollectionOptions {
/** A keyboard delegate to implement keyboard navigation in this collection. */
keyboardDelegate: KeyboardDelegate,
/** A function that returns a drop target at a given point. */
getDropTargetFromPoint: (x: number, y: number) => DropTarget | null,
}
interface DroppableCollectionResult {
/** Props for the collection element. */
collectionProps: HTMLAttributes<HTMLElement>
}
declare function useDroppableCollection(options: DroppableCollectionOptions, state: DroppableCollectionState, ref: RefObject<HTMLElement>): DroppableCollectionResult;
The useDroppableItem hook goes along with useDroppableCollection and handles drops for individual items within a collection. It provides props to be applied to the item, including a description for screen readers to specify how to perform a drop.
interface DroppableItemOptions {
/** The drop target represented by this element. */
target: DropTarget,
}
interface DroppableItemResult {
/** Props for the item element. */
dropProps: HTMLAttributes<HTMLElement>
}
declare function useDroppableItem(options: DroppableItemOptions, state: DroppableCollectionState, ref: RefObject<HTMLElement>): DroppableItemResult;
The useDropIndicator hook works with useDroppableCollection to provide props for an insertion indicator between two items in a collection. This includes an accessibility label to identify which elements it is between, and a description to specify how to perform a drop.
interface DropIndicatorProps {
/** The drop target represented by the drop indicator. */
target: DropTarget
}
interface DropIndicatorAria {
/** Props for the drop indicator element. */
dropIndicatorProps: HTMLAttributes<HTMLElement>
}
declare function useInsertionIndicator(props: InsertionIndicatorProps, state: DroppableCollectionState, ref: RefObject<HTMLElement>): DropIndicatorAria;
Documentation for both the hooks in React Aria and higher level APIs for drag and drop in React Spectrum will be needed. Given that this is a complex topic that spans many components, it may be worthwhile to create separate pages to explain drag and drop in general rather than repeating all of the details on each component page.
Drag and drop is quite complex and will require a lot of work and testing. See the alternatives section below for more details on other options we considered to reduce this burden.
This is an additive change to our APIs, and should not cause any breaking changes. That said, the work to enable drag and drop will touch many components, as well as low level hooks like usePress
. There is some danger in doing this, and we will need to carefully test our changes across devices to ensure we don’t unintentionally introduce behavioral regressions.
Investing in building fully accessible drag and drop patterns is a lot of work. We considered recommending that product teams simply support drag and drop in addition to an alternative method, such as a menu or modal for moving or copying items between locations, as many desktop applications support. However, it is likely that many applications would not or could not support this either for lack of time or resources, or because they are unaware they need to do so for accessibility. In addition, drag and drop often allows an easier and more intuitive way of performing many tasks, such as choosing a specific location to insert an item. Thus, making the drag and drop pattern itself accessible rather than leaving it up to products to choose alternative interactions was chosen as a path forward.
As discussed above, there are some open questions for keyboard and screen reader interactions about how to trigger drag activation, and how to allow users to choose a drop operation when more than one is allowed.
In addition, once this has been built out more, it would be advantageous to test these interactions with real users to determine if the patterns developed here are understandable and intuitive.