Keyboard-Accessible Drag and Drop Alternatives
Drag and drop is the most exclusionary interaction pattern in common use. It requires a pointer, sustained fine motor control, and the ability to see where the item is going. WCAG 2.2 responded with 2.5.7 Dragging Movements, which requires a single-pointer alternative for any dragging action — and keyboard users have needed one since long before that.
The good news is that the alternative is usually simpler than the drag itself: a keyboard grab-and-move model, or a plain "Move to…" menu, both of which are easier to implement than a physics-accurate drag preview. This guide covers both, and the announcements that make either usable without sight.
It extends Keyboard Navigation Patterns for Modals into the reordering case.
WCAG Success Criteria Addressed:
2.1.1 Keyboard2.5.7 Dragging Movements2.5.8 Target Size (Minimum)4.1.3 Status Messages
Prerequisites
- A list or board whose items are already focusable — real
<button>elements or list items with a roving tabindex. - A polite live region for move announcements; see building a useAnnouncer hook for live regions.
- A data model where "move item X to position N" is a single operation. If reordering requires a drag preview to compute the target, the model needs fixing first.
Model the Interaction as Grab, Move, Drop
The keyboard model is a small state machine with three keys, and it maps cleanly onto the same reorder function the pointer drag calls.
Implementation Guidelines:
- Make each draggable item a real
<button>(or give ittabindex="0") so it can be focused and activated. SpaceorEntertoggles grabbed state; arrow keys move the grabbed item;Escapecancels and restores the original index.- Set
aria-grabbedsparingly — it is deprecated and inconsistently supported. Announce the state through the live region instead, which works everywhere. - Reuse the same
moveItem(from, to)function the pointer path calls, so both interactions cannot drift apart.
'use client';
export function useKeyboardReorder(items: Item[], move: (from: number, to: number) => void) {
const [grabbed, setGrabbed] = useState<number | null>(null);
const origin = useRef<number | null>(null);
return function onKeyDown(event: React.KeyboardEvent, index: number) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
if (grabbed === null) { origin.current = index; setGrabbed(index); }
else { setGrabbed(null); origin.current = null; }
return;
}
if (grabbed === null) return;
if (event.key === 'ArrowDown' && grabbed < items.length - 1) {
event.preventDefault();
move(grabbed, grabbed + 1);
setGrabbed(grabbed + 1);
}
if (event.key === 'Escape' && origin.current !== null) {
event.preventDefault();
// A11y rationale: 2.1.1 Keyboard — cancel must be as available as commit,
// or a mistaken grab becomes an unrecoverable reorder.
move(grabbed, origin.current);
setGrabbed(null);
}
};
}
Announce Every Move
Without announcements the keyboard model is unusable without sight: the user grabs an item and then has no idea where it is.
Implementation Guidelines:
- Announce the grab: "Grabbed Deploy to staging. Use arrow keys to move, Space to drop, Escape to cancel."
- Announce each move with the new position in context: "Moved to position 3 of 7."
- Announce the drop with the final position, and the cancel with the restoration: "Returned to position 5 of 7."
- Keep messages short — they fire repeatedly during a move, so every extra word is repeated on every arrow press.
announce(`Moved to position ${to + 1} of ${items.length}`);
Or Skip Dragging Entirely
For many interfaces the best keyboard alternative is not a keyboard drag at all — it is a menu. "Move to…" is faster than nine arrow presses, works on touch, and needs no state machine.
Implementation Guidelines:
- Offer a "Move to…" control on every draggable item, opening a menu of destinations ("Top", "Up one", "Down one", "Bottom", or named columns for a board).
- For short lists, up/down buttons are simpler still — and their names should say what they do: "Move Deploy to staging up".
- For moves between containers, a two-step mark-and-place model works well and needs no drag preview.
- Whichever you choose, keep the pointer drag as well. The alternative is an addition, not a replacement.
Target Size and Motor Load
2.5.7 is about the gesture; 2.5.8 is about the target. Drag handles fail both more often than any other control.
Implementation Guidelines:
- Make the handle at least 24×24 CSS pixels — many are 16-pixel dot grids, which is below the minimum.
- Avoid requiring precision at the drop site: snap to positions rather than demanding pixel-accurate placement.
- Never make the drag the only way to perform a destructive action (swipe-to-delete with no button), since an accidental gesture then has no undo path.
- Provide undo for reorders. It benefits everyone and it is the difference between a mistake and a disaster for someone with a tremor.
How to Verify
- Keyboard-only reorder. Move an item three positions with the keyboard, then cancel a second move with Escape. Both must work without touching a pointer.
- Screen reader pass. Confirm the grab, each move and the drop are announced, and that the messages are short enough not to lag behind the key presses.
- Single-pointer test. Perform every drag operation using only taps — no press-and-hold path. If any action cannot be completed,
2.5.7is not satisfied. - Handle measurement. Measure the drag handle in DevTools; under 24×24 CSS pixels fails
2.5.8. - Undo check. Reorder, then undo. Confirm the list and the announcement both reflect the restoration.
Common Accessibility Mistakes
- Shipping drag as the only path. The most common
2.5.7failure, and it excludes keyboard, switch and many touch users at once. - A keyboard model with no announcements. The item moves invisibly; the user has no idea where it is.
- No cancel. Once grabbed, the item must go somewhere, so a mis-press becomes a data change.
- Relying on
aria-grabbed. Deprecated and unevenly supported; use live-region announcements instead. - Drag handles under 24 pixels. Standard in dense list UIs and a straightforward target-size failure.
- Different code paths for pointer and keyboard. They drift, and the keyboard one is the one nobody notices breaking.
Conclusion
Every drag interaction needs a second path, and the second path is usually the cheaper one to build. A three-key state machine — grab, move, drop, with Escape to cancel and an announcement on every step — covers keyboard and screen reader users. A "Move to…" menu covers touch, switch and anyone who finds nine arrow presses tedious. Both call the same reorder function the pointer drag calls, which is what keeps them honest. Build the alternative first and the drag becomes what it should have been all along: an accelerator, not a requirement.
It is worth stating what the alternative buys beyond conformance. A move-to menu is faster than dragging for anyone reordering a long list, works on a phone where drag targets are cramped, and is trivially testable in a way pointer gestures never are. Teams that build it first usually find the drag becomes a nicety rather than the primary interaction — which is a better outcome for the product as well as a 2.5.7 pass, and it means the untested path is the optional one rather than the required one.
Frequently Asked Questions
Does 2.5.7 mean drag and drop is banned?
No. It requires that any functionality using a dragging movement also be operable with a single pointer without dragging — a tap-based alternative. The drag can stay; it simply cannot be the only way. Note the exception for cases where dragging is essential to the function, which is rarer than people assume.
Is HTML5 drag and drop accessible? The native API is pointer-only and does not expose drag state to assistive technology in any usable way. Most accessible implementations avoid it entirely, using pointer events for the visual drag and a separate keyboard model — which is exactly what the major React drag libraries do.
Should the drag handle or the whole item be draggable? A dedicated handle is usually better: it keeps the rest of the item clickable, and it gives the keyboard model an obvious target to attach to. Make sure the handle is a real button with a name like "Reorder Deploy to staging", not an unlabelled icon.
How do I announce moves in a two-dimensional board? Include both axes and the container name: "Moved to In progress, position 2 of 5". Column changes matter more than row changes, so announce the column first — it is the part the user is most likely to have got wrong.
What about sortable tables?
Column sorting is a different interaction — a button in the header with aria-sort — and it is nearly always the better choice for tabular data than row dragging. See building a sortable, accessible data table in React.
Related guides
- Keyboard Navigation Patterns for Modals — the parent guide on keyboard interaction models.
- Escape Key & Click-Outside Patterns for Overlays — the cancel semantics this pattern borrows.
- Building a useAnnouncer Hook for Live Regions — the announcement channel for every move.
- Building a Sortable, Accessible Data Table in React — the tabular alternative to row dragging.