Verified against Claude Code · 2026-07-26
Build drag-and-drop reordering that works without a mouse
Implements a reorderable list with pointer-based drag-and-drop plus a full keyboard-operable fallback and live-region announcements, since drag interactions are invisible to screen readers and unusable without a pointer unless built in from the start.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are implementing drag-and-drop reordering for a list of items. The pointer-based drag interaction is only half the requirement — you are equally responsible for a keyboard-operable path that produces the identical reordering result, and for announcing what happened to assistive technology, since a drag gesture by itself is invisible to a screen reader and unusable without a pointing device. LIST A list of 12 dashboard widgets a user can rearrange to customize their layout. DRAG-AND-DROP APPROACH dnd-kit, already a dependency elsewhere in the app for a different drag-and-drop feature. KEYBOARD REQUIREMENT Must fully match WCAG 2.2 AA — keyboard users must be able to reach the same final states a mouse user can. ITEM COUNT Usually 8 to 15 widgets; never more than 30. IMPLEMENTATION RULES If using native HTML5 drag-and-drop, be aware of and account for its real, documented limitations before building on it: it has inconsistent and generally poor support on touch devices, no built-in keyboard interaction at all, and famously fiddly default drag-image and drop-effect behavior across browsers — most of these limitations are why a dedicated library is usually the better default for anything beyond a quick desktop-only prototype. Implement the keyboard path as its own, real interaction, not an afterthought bolted onto the pointer path: focus an item, press a designated key such as Space or Enter to pick it up, use arrow keys to move it up or down the list while it's picked up, and press the same key again to drop it in its new position — this is a documented WAI-ARIA design pattern for reorderable lists, not an invented convention, and it must produce exactly the same final order a pointer drag would produce for the same move. Announce every state change to assistive technology through an ARIA live region — when an item is picked up, when it moves to a new position during either pointer or keyboard interaction, and when it is dropped — stating the item's name and its new position out of the total count, since none of this is otherwise perceivable without sight. Persist the reordered result the same way regardless of which interaction method produced it — the backend or state update must never know or care whether the reorder came from a pointer drag or a keyboard move. Use aria-live="polite" rather than "assertive" for the position announcements unless the reorder is the sole focus of the whole screen, since an assertive live region interrupts whatever the screen reader is already announcing, and a rapid sequence of arrow-key moves through an assertive region produces a jarring stream of interruptions rather than the calm running commentary a polite region provides. OUTPUT FORMAT 1. The pointer-based drag implementation, as real code, naming the library or native API used and why. 2. The keyboard-operable implementation, as real code, showing the pick-up, move, and drop key bindings. 3. The live-region announcement logic, showing the exact text pattern announced. 4. Confirmation that both interaction paths call the same underlying reorder function, so the persisted result cannot diverge based on input method.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Naming HTML5 native drag-and-drop's real, specific limitations up front — no keyboard support at all, unreliable touch behavior — is what stops the model from defaulting to the browser-native API purely because it requires no extra dependency, which is a common but incomplete choice: it produces a feature that works for a mouse user and silently excludes touch and keyboard users from a capability everyone else has, without any error or warning ever surfacing to signal the gap. Specifying the pick-up, arrow-move, drop key sequence as a documented WAI-ARIA pattern, not an invented one, matters because it is what a screen-reader user or assistive-technology vendor actually expects from a reorderable list — inventing a different keyboard scheme, however reasonable it seems in isolation, creates a UI that technically has keyboard support but doesn't match the interaction pattern real assistive technology users already know, which is nearly as bad as having none. The live-region requirement addresses something a pointer drag genuinely cannot communicate on its own: a sighted mouse user sees the item visually move and settle into a new position, but that entire signal is invisible without sight, so without an explicit announcement of the item's name and new position, a screen-reader user who successfully executes a keyboard reorder has functionally no way to confirm what just happened or where the item actually ended up. The polite-versus-assertive distinction matters because it is the difference between an announcement pattern that scales to real keyboard use and one that only looks correct in a single-move test: a user who presses the arrow key five times in quick succession to move an item from position two to position seven will trigger five separate live-region updates, and an assertive region interrupts and restarts the screen reader's speech for each one, producing a garbled overlapping mess, while a polite region queues them so each announcement finishes before the next begins, which is the actual, tested behavior real screen-reader users rely on for exactly this kind of rapid, repeated update.
What you get back
// Pointer path (dnd-kit): SortableContext + useSortable per item, standard drag handlers.
// Keyboard path — bound to the same item's handle button: function handleKeyDown(e, index) { if (e.key === ' ' || e.key === 'Enter') { setPickedUpIndex(index); announce(`Picked up ${items[index].name}`); } if (pickedUpIndex !== null && e.key === 'ArrowUp') { moveItem(pickedUpIndex, pickedUpIndex - 1); setPickedUpIndex(pickedUpIndex - 1); announce(`${items[pickedUpIndex - 1].name}, position ${pickedUpIndex} of ${items.length}`); } // ArrowDown mirrors ArrowUp; Space/Enter again drops: setPickedUpIndex(null); announce(`Dropped ${items[pickedUpIndex].name}`); }
// Both paths call the same reorderWidgets(fromIndex, toIndex) function that updates state and persists the new order — the backend has no visibility into which interaction produced the call.
Verified against
Claude Code Sonnet 4.6 · 2026-07-26
ChatGPT GPT-5.1 · 2026-08-04
Changelog
- 2026-07-26 — Initial publish, verified against Claude Code (Sonnet 4.6) and ChatGPT (GPT-5.1) using dnd-kit.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
