core accessibility principles for modern frameworks

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 Keyboard
  • 2.5.7 Dragging Movements
  • 2.5.8 Target Size (Minimum)
  • 4.1.3 Status Messages
The keyboard grab, move and drop model A three-state machine. In the idle state the item is a focusable button and arrow keys move focus between items as normal. Pressing Space or Enter grabs the item, entering the grabbed state, where arrow keys move the item itself and each move is announced with its new position. Pressing Space or Enter again drops it, announcing the final position; pressing Escape instead cancels and returns the item to where it started. Idle arrows move focus between items Grabbed arrows move the item each move announced Dropped final position announced once Space Space Escape — cancel and return the item to its original position Three keys carry the whole interaction: Space to grab and drop, arrows to move, Escape to cancel.

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 it tabindex="0") so it can be focused and activated.
  • Space or Enter toggles grabbed state; arrow keys move the grabbed item; Escape cancels and restores the original index.
  • Set aria-grabbed sparingly — 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.

Three single-pointer alternatives to dragging Three approaches compared by what they suit. A move-to menu on each item lists the possible destinations and is best for long lists and boards, since any position is one action away. Up and down buttons beside each item are best for short lists where items usually move one place. A cut-and-paste model, marking an item then choosing a destination, suits moving items between containers. All three satisfy the dragging movements criterion because none requires a path-based gesture. "Move to…" menu lists every destination one action per move scales to long lists best for boards Up / down buttons one press per position no state to hold visible affordance best for short lists Mark and place select, then choose a destination works across containers best for moving between lists All three satisfy 2.5.7 Dragging Movements: none requires a path-based gesture, and each works with one pointer, one finger, a keyboard, or a switch device.

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.
Drag handle sizes against the target-size minimum Two drag handles drawn to scale beside the twenty-four pixel minimum. A typical six-dot grip rendered at sixteen by sixteen pixels falls below the minimum and fails the criterion. The same grip glyph inside a button padded to twenty-eight by twenty-eight pixels passes, with the glyph itself unchanged. A note explains that padding the control rather than scaling the icon keeps the visual design intact. 24 × 24 px minimum (2.5.8) 16 × 16 px grip — the common default ✗ below the minimum Same glyph, padded to 28 × 28 px ✓ passes, design unchanged Pad the control, never scale the icon — the glyph stays crisp and the hit area grows where it matters.

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.7 is 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.7 failure, 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.