Native HTML drag-and-drop is inaccessible. How do you make a reorder/DnD interaction work for keyboard and screen-reader users?
Providing a non-pointer alternative for an inherently pointer-based interaction and announcing it.
The HTML5 Drag and Drop API is effectively unusable by keyboard and poorly supported by screen readers, so an accessible reorder cannot rely on it alone — you must provide an equivalent keyboard operation and announce what happens. The established pattern (WAI-ARIA APG's 'keyboard' approach, as popularised by libraries like dnd-kit and react-aria) has a few parts. (1) Each draggable item is focusable and has a clear accessible name and instructions (e.g. via aria-describedby: 'Press Space to pick up, arrow keys to move, Space to drop, Escape to cancel'). (2) A GRABBED state: on Space/Enter the item enters 'picked up' mode; arrow keys then move it among positions; a second Space drops it; Escape cancels and returns it to its origin. (While grabbing, aria-grabbed / aria-dropeffect existed in ARIA 1.0 but are deprecated — modern implementations manage state and announcements manually rather than relying on them.) (3) ANNOUNCE every step through a polite live region: 'Item 3 grabbed, position 3 of 5', 'moved to position 2 of 5', 'dropped at position 2' — without this a screen-reader user has no feedback that anything moved. (4) Move real FOCUS with the item so the keyboard user follows it. (5) Provide a NON-DnD fallback control too where practical (up/down buttons, a 'move to' menu) since keyboard DnD is still cognitively heavy. Also honour prefers-reduced-motion for any drag animation. The senior point: DnD is a pointer gesture; accessibility means offering a parallel, fully-announced keyboard model — not trying to make mouse dragging itself keyboard-driven.
Any sortable list, kanban, or reorder UI; giving drag-and-drop a keyboard + announced alternative.
function SortableItem({ label, index, total, onMove, announce }) {
const [grabbed, setGrabbed] = React.useState(false);
function onKeyDown(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
setGrabbed(g => !g);
announce(grabbed ? 'Dropped at ' + (index + 1) : 'Grabbed, ' + (index + 1) + ' of ' + total);
} else if (grabbed && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
e.preventDefault();
const to = index + (e.key === 'ArrowDown' ? 1 : -1);
if (to >= 0 && to < total) { onMove(index, to); announce('Moved to ' + (to + 1) + ' of ' + total); }
} else if (e.key === 'Escape' && grabbed) { setGrabbed(false); announce('Cancelled'); }
}
return (
<li tabIndex={0} aria-describedby="dnd-help" onKeyDown={onKeyDown}
style={{ outline: grabbed ? '2px solid #2563eb' : undefined }}>
{label}
</li>
);
}
// <p id="dnd-help" class="sr-only">Space to pick up, arrows to move, Space to drop, Escape to cancel</p>
// <div aria-live="polite" class="sr-only">{message}</div>