QuestionsAccessibility

Keyboard-accessible drag and drop

Keyboard InteractionHardAccessibility

Native HTML drag-and-drop is inaccessible. How do you make a reorder/DnD interaction work for keyboard and screen-reader users?

What it tests

Providing a non-pointer alternative for an inherently pointer-based interaction and announcing it.

Approach & answer

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.

Use this technique when

Any sortable list, kanban, or reorder UI; giving drag-and-drop a keyboard + announced alternative.

Code

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>

References