Yugen UI

Sortable

Sortable reorders lists by drag and drop or with the keyboard; every step is announced to screen readers.

Sortable is a list people can reorder: SortableItem is each item, SortableHandle its handle. Drag the handle with a mouse, pen or finger; an item swaps places once it passes the middle of its neighbor, and the frame scrolls by itself near its edges. With a keyboard: focus the handle, press Space or Enter to pick the item up, the arrow keys to move it, Space or Enter to drop it and Escape to cancel; each step is announced through useAnnounce. Without dragging, click a handle to pick its item up, then click another item's handle to put it there. 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.

TSX
// 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.

1Fast
2Cheap
3Durable

Horizontal

orientation="horizontal": ← → move the item, flipped in RTL.

Sakura
Fuji
Kyoto
Nara
Nikko
Osaka

Grid

orientation="grid": items wrap; ← → go to the next item, ↑ ↓ to the row above or below.

To do

Sign-in screen
Order list
Search

In progress

Payment

Done

Release notes

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

Do
  • Save the order in onSortEnd; onValueChange also runs during the drag.
  • Give SortableItem a label when 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
  • Don't put a SortableHandle inside an item with role option or menuitem: those can't contain buttons. Use handle and shortcuts.
  • Don't make a list draggable when its order is automatic (by date, by name).
  • Don't change an item's id between renders: key and value must stay stable.

Props

Prop · TypeDefaultDescription
valuestring[]Default: —Item ids in order. Leave it out inside a SortableGroup.
onValueChange(value: string[]) => voidDefault: —Called each time the order changes, including during a drag; Escape calls it again with the order from pick-up.
onSortStart(data: { item, from }) => voidDefault: —When an item is picked up.
onSortEnd(data: { item, from, to, canceled }) => voidDefault: —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.
listIdstringDefault: —The list's key in the SortableGroup value.
labelstringDefault: —The list name in announcements when an item changes lists. Defaults to aria-label / aria-labelledby.
disabledbooleanDefault: falseItems in this list can't be dragged and other items can't be dropped in.
asChildbooleanDefault: falseAttach to the child element (List, ul, tbody) instead of rendering a div.
SortableGroup.valueRecord<string, string[]>Default: —The order of every list, by listId.
SortableGroup.onValueChange(value: Record<string, string[]>) => voidDefault: —Like Sortable's onValueChange, for the whole group.
SortableGroup.onSortStart / onSortEnd(data) => voidDefault: —Like Sortable's; from.list / to.list tell which list the item is in.
SortableItem.valuestringDefault: —The item id, present in the list's value.
SortableItem.labelstringDefault: —The item name in announcements and in the handle label. Defaults to the item's text.
SortableItem.disabledbooleanDefault: falseCan't be picked up; the item still moves aside when others pass.
SortableItem.handlebooleanDefault: falseThe whole item is the handle for mouse / touch dragging.
SortableItem.shortcutsbooleanDefault: falseAlt + arrow keys on the item move it right away, without picking it up.
SortableItem.asChildbooleanDefault: falseAttach to the child element (ListItem, li, tr, Card) instead of rendering a div.
SortableHandle.asChildbooleanDefault: falseUse the child element as the handle instead of the default icon button.
arrayMove(array, from, to)T[]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 · TypeDefault (English)messagesVi
handlestring

Nhãn tay nắm khi mục không có label.

Default (English)"Reorder"messagesVi"Sắp xếp lại"
handleItemstring

Nhãn tay nắm của mục có label.

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ỷ."
liftedstringDefault (English)"Picked up {item}, position {position} of {total}."messagesVi"Đã nhấc {item}, vị trí {position} trên {total}."
movedstringDefault (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}."
droppedstringDefault (English)"Dropped {item} at position {position} of {total}."messagesVi"Đã thả {item} ở vị trí {position} trên {total}."
droppedInstringDefault (English)"Dropped {item} in {list}, position {position} of {total}."messagesVi"Đã thả {item} vào {list}, vị trí {position} trên {total}."
canceledstringDefault (English)"Canceled. {item} is back at position {position} of {total}."messagesVi"Đã huỷ. {item} về lại vị trí {position} trên {total}."