Sortable component
Sortable puts the items of a list, row or grid in a new order. A mouse or pen drags an item by its handle, a finger holds and then drags, and the keyboard picks an item up, moves it with the arrow keys and drops it, with every step announced. You keep the items; onReorder gives you the new order. To move things between areas use Draggable and DropZone, and between Kanban columns use Board.
Playground
Change a prop on the right and the preview updates. Switch to Code to copy what you see.
A reorderable list
Give items, a getKey that returns a unique stable key, and renderItem. getLabel is the name read aloud when an item is picked up and moved. onReorder is called once per drop with the items in their new order and { key, from, to }.
const [tasks, setTasks] = useState(initialTasks);
<Sortable
aria-label="Tasks"
items={tasks}
getKey={(t) => t.id}
getLabel={(t) => t.title}
renderItem={(t) => <Card>{t.title}</Card>}
onReorder={(next) => setTasks(next)}
/>Grids and dragging from anywhere
layout is vertical, horizontal or grid (set --oks-sortable-columns to change the columns). dragFromItem lets a pointer drag from anywhere on an item instead of only the handle; inputs, buttons and links inside still work as usual.
<Sortable
aria-label="Gallery order"
layout="grid"
dragFromItem
items={images}
getKey={(img) => img.id}
getLabel={(img) => img.alt}
renderItem={(img, { isDragging }) => <img src={img.src} alt={img.alt} data-faded={isDragging} />}
onReorder={(next, { from, to }) => save(next, from, to)}
/>Props
The full public API for Sortable. Every prop is stable and intentionally typed.
Data
| Prop | Type | Default | Description |
|---|---|---|---|
| items | T[] | — | The items, in their current order. |
| getKey | (item: T) => string | — | A unique, stable key for an item. |
| onReorder | (items: T[], move: { key, from, to }) => void | — | Called once per drop with the items in their new order and the 0-based old and new positions. |
items
T[]
default: —
The items, in their current order.
getKey
(item: T) => string
default: —
A unique, stable key for an item.
onReorder
(items: T[], move: { key, from, to }) => void
default: —
Called once per drop with the items in their new order and the 0-based old and new positions.
Content
| Prop | Type | Default | Description |
|---|---|---|---|
| getLabel | (item: T) => string | the key | The item's name, read when it is picked up and moved. |
| renderItem | (item: T, state) => ReactNode | — | Draws an item. state has index, isDragging (pointer) and isLifted (keyboard). |
| labels | Partial<SortableLabels> | English | handle(name), hint, pickedUp, movedTo, dropped, cancelled. Also set app-wide with LocaleProvider. |
| aria-label | string | — | Names the list. |
getLabel
(item: T) => string
default: the key
The item's name, read when it is picked up and moved.
renderItem
(item: T, state) => ReactNode
default: —
Draws an item. state has index, isDragging (pointer) and isLifted (keyboard).
labels
Partial<SortableLabels>
default: English
handle(name), hint, pickedUp, movedTo, dropped, cancelled. Also set app-wide with LocaleProvider.
aria-label
string
default: —
Names the list.
Appearance
| Prop | Type | Default | Description |
|---|---|---|---|
| layout | "vertical" | "horizontal" | "grid" | vertical | How the items flow. horizontal wraps; grid uses --oks-sortable-columns. |
| classNames | Partial<Record<slot, string>> | — | Class per part: base, item, handle, content. |
layout
"vertical" | "horizontal" | "grid"
default: vertical
How the items flow. horizontal wraps; grid uses --oks-sortable-columns.
classNames
Partial<Record<slot, string>>
default: —
Class per part: base, item, handle, content.
Behavior & states
| Prop | Type | Default | Description |
|---|---|---|---|
| dragFromItem | boolean | false | A pointer can drag from anywhere on an item, not only its handle. Inputs, buttons and links inside still work. |
| isDisabled | boolean | false | Nothing can be moved. |
dragFromItem
boolean
default: false
A pointer can drag from anywhere on an item, not only its handle. Inputs, buttons and links inside still work.
isDisabled
boolean
default: false
Nothing can be moved.
Accessibility
- A list of list items. Each handle is a button named Move and the item name, with a hidden hint linked through aria-describedby and aria-pressed while the item is picked up.
- On a handle: Space or Enter picks up, the arrow keys move, Home and End go to the ends, Space or Enter drops and Escape cancels and puts the item back. Left and right swap in right-to-left pages.
- Every pick up, move, drop and cancel is announced through a status region, for example Dropped Task 2 at position 4 of 6.
- A pointer drag starts after 5px of movement. A finger holds for 400ms first so the page still scrolls, or drags at once from the handle. Escape cancels a pointer drag.
Best practices
- Keep getKey stable. Using the array index breaks the announcements and the order after a drop.
- Give getLabel a short human name, not an id.
- Save the new order in onReorder; Sortable does not keep your items.
- Translate the announcements with the labels prop or LocaleProvider.