Sortable makes a list reorderable. Wrap the list, wrap each row in Sortable.Item, and apply the new order in onReorder. The rows themselves stay whatever they were: NavItems, Cards, task rows.
- Mouse and pen: the drag starts after a 4px move, so a click still clicks.
- Touch: press and hold a row for 200ms, so a tap still taps and a swipe still scrolls. A Sortable.Handle drags at once.
- Keyboard: Space picks a row up, the arrow keys move it, Space drops it and Escape puts it back. Screen readers hear "Picked up Launch plan, 1 of 5" and every step after.
- Reduced motion: the rows jump into place instead of sliding.
Sortable keeps no order of its own. It reports the move and shows whatever order items holds, so the order lives in your state, next to the data.
onReorder gets the new order of ids and the move that made it. When your rows are objects rather than ids, apply the move with arrayMove:
Lists with the same group trade rows. Give each one a name, and onMove(id, to, from) says where a row went, as { list, index } on both sides. Inside a Nav the rows keep the nav's spacing, and a closed NavGroup opens when a row is held over it.
With the keyboard, an arrow key past the end of a list carries the row on into the next list of its group. label names the list for screen readers: "Launch plan, 1 of 4 in Projects".
Keep a "Move to" item in each row's menu as well. Dragging is the quick way, not the only one.
With handle, a row drags only from its Sortable.Handle. Use it when the rows hold their own controls, such as a checkbox, or text people select. The handle is a button named "Reorder" plus the row's label, and on a touch screen it drags straight away with no press and hold.
The same group works for columns side by side. A Sortable.Item takes the Flex props, so a row can be the card itself. Use direction="row" for a horizontal list, such as a strip of media.
An empty list keeps 40px of room so a row can still be dropped into it.
A row that holds a link or a button keeps that as its one tab stop, and Space on it picks the row up. A row with nothing focusable becomes a tab stop itself. Each tab stop is described with the keys to use, and one live region reads out every pick up, move, drop and cancel. Pass labels to translate them: