Sortable
Sortable reorders lists by drag and drop or with the keyboard; every step is announced to screen readers.
onValueChange runs during the drag so the UI keeps up; save the order in onSortEnd. SortableGroup joins several lists (Kanban columns): items can move to another list, with the arrow keys across the list direction. When an item already uses Space / Enter for something else (a button, menu item or Tag), set handle to make the whole item the handle and shortcuts to move it with Alt + arrow keys; DataGridColumnPicker and TagPicker (reorderable) work this way. Horizontal and grid lists follow RTL; with reduced motion on, items swap without sliding. No drag-and-drop library underneath.- Sign-in screenDue Friday
- Order listWaiting on API
- PaymentHigh priority
- Release notesAfter launch
Import
Install from npm, copy the source into your project with @yugen-ui/cli or the shadcn CLI, or import directly when working in this repo.
// npm install yugen-uiimport { Sortable, SortableGroup, SortableItem, SortableHandle, arrayMove } from "yugen-ui";Variants
Each tile is a standalone example: hover to copy, or press Code to see the source.
Horizontal
orientation="horizontal": ← → move the item, flipped in RTL.
Grid
orientation="grid": items wrap; ← → go to the next item, ↑ ↓ to the row above or below.
To do
In progress
Done
Kanban: SortableGroup
Each column is a Sortable with a listId; SortableGroup holds the order of every column. With a keyboard, ← → move a card to the next column. The column receiving a card has data-over. onSortEnd runs once on drop: the place to save.
- Cherry Blossom
- June Rain
- Night Sea
- Monsoon
Move up / down commands
Add commands in a menu for people who don't notice the handle. arrayMove returns a new array; useAnnounce reads the new position as dragging does.
The whole item as handle
A button already uses Space / Enter to open: handle lets the whole button be dragged (a click without dragging is still a click; on touch screens, hold briefly then drag), shortcuts moves it with Alt + arrow keys. The buttons wrap in narrow frames, hence orientation="grid".
Usage
- Save the order in onSortEnd; onValueChange also runs during the drag.
- Give SortableItem a
labelwhen the item has a lot of text, so announcements stay short. - Add Move up / down commands when people may not notice the handle.
- Don't put a SortableHandle inside an item with role option or menuitem: those can't contain buttons. Use
handleandshortcuts. - Don't make a list draggable when its order is automatic (by date, by name).
- Don't change an item's id between renders:
keyandvaluemust stay stable.
Props
| Prop · Type | Default | Description |
|---|---|---|
| valuestring[] | Default: — | Item ids in order. Leave it out inside a SortableGroup. |
| onValueChange(value: string[]) => void | Default: — | Called each time the order changes, including during a drag; Escape calls it again with the order from pick-up. |
| onSortStart(data: { item, from }) => void | Default: — | When an item is picked up. |
| onSortEnd(data: { item, from, to, canceled }) => void | Default: — | Once per drop, including moves with the shortcut. from / to are { list, index }; canceled on Escape. Save the order here. |
| orientation"vertical" | "horizontal" | "grid" | Default: "vertical" | The list direction: which arrow keys move an item and how the drop position is found while dragging. |
| listIdstring | Default: — | The list's key in the SortableGroup value. |
| labelstring | Default: — | The list name in announcements when an item changes lists. Defaults to aria-label / aria-labelledby. |
| disabledboolean | Default: false | Items in this list can't be dragged and other items can't be dropped in. |
| asChildboolean | Default: false | Attach to the child element (List, ul, tbody) instead of rendering a div. |
| SortableGroup | Default: — | The order of every list, by listId. |
| SortableGroup | Default: — | Like Sortable's onValueChange, for the whole group. |
| SortableGroup | Default: — | Like Sortable's; from.list / to.list tell which list the item is in. |
| SortableItem | Default: — | The item id, present in the list's value. |
| SortableItem | Default: — | The item name in announcements and in the handle label. Defaults to the item's text. |
| SortableItem | Default: false | Can't be picked up; the item still moves aside when others pass. |
| SortableItem | Default: false | The whole item is the handle for mouse / touch dragging. |
| SortableItem | Default: false | Alt + arrow keys on the item move it right away, without picking it up. |
| SortableItem | Default: false | Attach to the child element (ListItem, li, tr, Card) instead of rendering a div. |
| SortableHandle | Default: false | Use the child element as the handle instead of the default icon button. |
| arrayMove | Default: — | A new array with the element at from moved to to. |
Messages
Text built into the component: labels, aria-labels, screen reader announcements.
English by default. Change it with the messages prop of YugenProvider, by the namespace below; messagesVi has the Vietnamese set. See Interface language.
messages.sortable
| Key · Type | Default (English) | messagesVi |
|---|---|---|
| handlestring Nhãn tay nắm khi mục không có | Default (English)"Reorder" | messagesVi"Sắp xếp lại" |
| handleItemstring Nhãn tay nắm của mục có | Default (English)"Reorder {item}" | messagesVi"Sắp xếp lại {item}" |
| instructionsstring Hướng dẫn dùng bàn phím, là mô tả (aria-describedby) của tay nắm. | Default (English)"Press Space or Enter to pick up the item, arrow keys to move it, Space or Enter to drop it, Escape to cancel." | messagesVi"Nhấn Space hoặc Enter để nhấc mục lên, dùng phím mũi tên để di chuyển, Space hoặc Enter để thả, Esc để huỷ." |
| liftedstring | Default (English)"Picked up {item}, position {position} of {total}." | messagesVi"Đã nhấc {item}, vị trí {position} trên {total}." |
| movedstring | Default (English)"{item}: position {position} of {total}." | messagesVi"{item}: vị trí {position} trên {total}." |
| movedTostring Mục vừa sang danh sách khác (SortableGroup). | Default (English)"{item}: moved to {list}, position {position} of {total}." | messagesVi"{item}: sang {list}, vị trí {position} trên {total}." |
| droppedstring | Default (English)"Dropped {item} at position {position} of {total}." | messagesVi"Đã thả {item} ở vị trí {position} trên {total}." |
| droppedInstring | Default (English)"Dropped {item} in {list}, position {position} of {total}." | messagesVi"Đã thả {item} vào {list}, vị trí {position} trên {total}." |
| canceledstring | Default (English)"Canceled. {item} is back at position {position} of {total}." | messagesVi"Đã huỷ. {item} về lại vị trí {position} trên {total}." |