Press Space or Enter to pick up a row. Use the arrow keys to move it, Space or Enter to drop it, and Escape to cancel.
A list you reorder by dragging: the row lifts off the page and follows the pointer, the other rows step aside to make room, the list scrolls on its own near a scroll edge, and on drop every row settles into its new place from where it was drawn. On touch a short long press lifts the row, so the list still scrolls under a passing finger. The keyboard does it all too: Space picks a row up, the arrows carry it, Space drops it and Escape puts it back, with every step announced to screen readers. Drag the whole row, or only its grip so the row can hold buttons and links. Controlled or uncontrolled, any item shape, your own row content.
The import and the props worth knowing about, in one place.
import { ListSortable } from "@/components/beste/component/list-sortable";
// Controlled, with your own row content
<ListSortable
items={tracks}
onReorder={(next) => setTracks(next)}
getItemLabel={(track) => track.title}
renderItem={(track, { index }) => (
<span className="flex flex-1 items-center justify-between">
<span>{index + 1}. {track.title}</span>
<span className="text-muted-foreground">{track.length}</span>
</span>
)}
/>
// Rows that hold buttons: only the grip drags
<ListSortable
items={tasks}
onReorder={setTasks}
handle
renderItem={(task) => (
<>
<span className="flex-1">{task.name}</span>
<button onClick={() => console.log("remove", task.id)}>Remove</button>
</>
)}
/>
// Plain strings, uncontrolled
<ListSortable defaultItems={["Vocals", "Guitar", "Drums"]} getKey={(item) => item} />Every one of these is in the component already. They are listed because a props table cannot mention a gesture, so nothing else on this page can tell you they exist.
| Drag | Lift a row and move it. The other rows make room, and the list scrolls near its edges. |
|---|---|
| Long press (touch) | Lift a row. A finger that moves first scrolls the list instead. |
| Space / Enter | Pick the focused row up, or drop it where it is. |
| Arrow keys | Carry the picked up row one place up or down. |
| Home / End | Carry it to the top or the bottom. |
| Escape | Put the row back where it started. Works mid drag too. |
Read from the component's own type, so this cannot drift from what it accepts.
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | The rows, in order. Pair with `onReorder` to control them. |
defaultItems | T[] | — | The starting rows when uncontrolled. |
onReorder | (items: T[], move: { from: number; to: number; item: T }) => void | — | Called with the reordered list when a row is dropped somewhere new. |
getKey | (item: T, index: number) => string | — | A stable key per row. Defaults to `item.id`; index keys break the settle animation. |
getItemLabel | (item: T) => string | — | What screen readers call a row. Defaults to `label`, `title` or `name`. |
renderItem | (item: T, state: SortableItemState) => React.ReactNode | — | The row's content. The grip and the surface are drawn around it. |
handle | boolean | — | Only the grip starts a drag, so the rest of the row can hold links and buttons. |
grip | boolean | true | Draw the grip at the start of each row. With `handle`, the grip is always drawn. |
tone | "muted" | "outline" | "ghost" | "outline" | |
size | "sm" | "default" | "lg" | "default" | |
disabled | boolean | — | |
aria-label | string | "Sortable list" | |
className | string | — | |
itemClassName | string | — |