DEV Community

137Foundry
137Foundry

Posted on

How to Add Keyboard Support to a Drag-and-Drop List

A reorderable list built only for mouse and touch drag is invisible to keyboard-only users, screen reader users, and anyone with a motor impairment that makes a precise drag gesture difficult. This walkthrough covers adding a full keyboard alternative to an existing pointer-based drag list without rebuilding the whole feature.

Done well, this isn't a compliance checkbox bolted onto an existing feature. Arrow-key reordering is often genuinely faster than a mouse drag for power users working through a long list, since moving an item five positions down takes five key presses with no risk of an imprecise drop, where a pointer drag requires sustained precision across the whole gesture.

Whiteboard covered in sketched user flow diagrams and arrows
Photo by ThisIsEngineering on Pexels

Step 1: Make Every List Item Focusable

Before keyboard reordering can work, each draggable item needs to be reachable with Tab. Set tabindex="0" on each item's drag handle (or the item itself, if the whole row is draggable) so keyboard users can move focus into the list the same way mouse users move a cursor into it.

Avoid tabindex values greater than 0. Positive tabindex values override the natural document order and create confusing focus jumps elsewhere on the page, which causes more accessibility problems than it solves.

Step 2: Define a "Grab" Mode Triggered by Space or Enter

With focus on an item, pressing Space or Enter should enter a "grab" mode: the item is now conceptually picked up, even though nothing has visually moved yet. Track this in component state (isGrabbed: true for the focused item) and give it a visual treatment matching the "lifted" state a mouse drag would show, a shadow or slight scale change.

This state needs an ARIA live region announcement the moment it activates, something like "Item grabbed. Use arrow keys to move, Enter to drop, Escape to cancel." Without an explicit announcement, a screen reader user has no way to know the interaction mode changed.

function handleKeyDown(e, item) {
  if (e.key === 'Enter' || e.key === ' ') {
    if (!grabbedItem) {
      setGrabbedItem(item);
      announce(`${item.label} grabbed. Use arrow keys to move, Enter to drop.`);
    } else {
      commitMove(grabbedItem);
      announce(`${item.label} dropped at new position.`);
      setGrabbedItem(null);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Move the Item With Arrow Keys

Once an item is grabbed, Up and Down arrow keys (or Left and Right for a horizontal list) should move it one position at a time within the underlying data array, re-rendering the list on each key press so the visual order updates immediately.

Each move should trigger its own live region announcement: "Moved to position 3 of 8." This is the part that's easy to skip and the part that matters most, since without positional announcements a screen reader user has no way to track where the item currently sits in the list.

Step 4: Confirm or Cancel the Move

Pressing Enter again while grabbed should commit the move and exit grab mode, matching the release of a mouse drag. Pressing Escape should cancel the operation entirely, returning the item to its original position and announcing that the move was canceled.

Both paths need to restore focus to the item's new (or original) position afterward. Losing focus during a keyboard interaction is a common accessibility bug, since it forces the user to re-navigate the entire list to find where their focus went.

Step 5: Wire ARIA Attributes for Screen Reader Context

Beyond live region announcements, the list itself benefits from aria-describedby pointing to a hidden instructions element ("Press Space to grab, arrow keys to move, Enter to drop, Escape to cancel"), so a screen reader user discovers the interaction pattern without needing external documentation.

The WAI-ARIA authoring guidance documents this exact pattern under its reorderable list examples, and following it closely matters because screen reader users have learned this convention from other well-built interfaces; deviating from it recreates a learning curve that shouldn't exist.

Step 6: Test With an Actual Screen Reader, Not Just DevTools

Automated accessibility linters will catch missing tabindex and missing ARIA attributes, but they cannot verify that the live region announcements are actually useful or correctly timed. Testing with VoiceOver on macOS or NVDA on Windows, navigating the list with only the keyboard and eyes closed, surfaces problems that no linter catches: an announcement that fires too early, one that's read in a confusing order, or a focus jump that leaves the user lost.

The MDN accessibility documentation covers the underlying live region and focus management APIs in more depth if any of the ARIA behavior above needs a refresher before implementation. Running an automated pass with axe DevTools before the manual screen reader test catches the structural issues (missing labels, incorrect roles) quickly, leaving the manual pass free to focus on timing and announcement quality that automated tools can't evaluate.

Step 7: Handle Multi-Select Reordering, If the List Needs It

Some lists support selecting multiple items before dragging, moving a batch of rows together rather than one at a time. The keyboard equivalent needs its own grab mode: a way to toggle selection on multiple items (commonly Shift plus arrow keys, or a dedicated selection key) before entering the same grab-move-confirm cycle described above, now operating on the whole selected group.

This is meaningfully more complex than single-item keyboard reordering, since the live region announcements need to communicate the size of the group being moved, not just a single item's new position, and the visual state needs to represent multiple simultaneously grabbed rows clearly. If the mouse-driven version of the feature doesn't support multi-select dragging, skip this step entirely rather than building keyboard-only functionality that has no pointer equivalent, since that asymmetry tends to confuse users switching between input methods.

What Good Keyboard Support Looks Like in Practice

A well-built keyboard alternative doesn't feel like an accommodation grafted onto the "real" feature. Users who never open a screen reader still benefit from it: a support engineer triaging a long queue of tickets, reordering by priority with arrow keys while keeping both hands on the keyboard, moves faster than someone switching between keyboard and mouse for every single reorder.

That's the strongest argument for building this in from the start rather than treating it as a lower-priority accessibility pass scheduled for later. It's not purely a compliance cost, it's a feature that a meaningful share of power users will actively prefer once they discover it exists, keyboard-only or not.

A Note on Testing With Real Assistive Technology Users

Internal testing with a screen reader, done by a sighted developer who isn't a daily screen reader user, catches the obvious gaps but misses subtler usability issues that only surface with real usage patterns. If the budget allows, even a short session with someone who uses a screen reader daily is worth more than hours of internal testing, since their navigation habits and expectations differ from what a sighted developer simulating the experience will naturally try.

Where that isn't feasible, at minimum have more than one person on the team run through the manual test pass, since different people tend to catch different gaps depending on how familiar they already are with the feature's intended behavior.

Why This Is Worth Building In Now

Retrofitting keyboard support after a mouse-only drag feature has already shipped usually means reworking the underlying state model to support two parallel ways of expressing "move this item." Building both from the same state model from the start, even if the keyboard path ships a sprint later than the mouse path, keeps the logic in one place and avoids that rework entirely.

The broader set of decisions behind building drag-and-drop well, drop target feedback, scroll container handling, and performance during the drag, is covered in the full guide on designing drag-and-drop interactions that work on touch and desktop.

For teams auditing an existing feature for gaps like this, 137Foundry's web development team covers more production UX and accessibility patterns worth knowing before the next release.

Top comments (0)