SortableList
A list the user reorders by dragging a row's handle or with the keyboard, such as dashboard widgets or a priority list. Each row has a grip handle; the rest of the row holds any content.
Preview
import { SortableList, SortableListItem } from "@/components/ui/sortable-list"
<SortableList label="Dashboard widgets" className="w-80 max-w-full">
<SortableListItem value="revenue" label="Revenue" />
<SortableListItem value="signups" label="Sign-ups" />
<SortableListItem value="churn" label="Churn" />
<SortableListItem value="traffic" label="Traffic" />
</SortableList>Usage
When to use
- Let someone set their own order, such as the widgets on a dashboard or a list of priorities.
- Reorder a short list by dragging or with the keyboard, saving each new order.
When not to use
- Order rows by a value such as a date or a name. Use Table with sortable columns.
- Show steps in a fixed order. Use Stepper.
- Pick several items from a list. Use Checkbox.
Variants
size
Row height and padding: sm rows are at least size-element-md tall for dense panels, md rows at least size-element-xl for content with two lines.
import { SortableList, SortableListItem } from "@/components/ui/sortable-list"
<SortableList label="Dashboard widgets" size="sm" className="w-80 max-w-full">
<SortableListItem value="revenue" label="Revenue" />
<SortableListItem value="signups" label="Sign-ups" />
<SortableListItem value="churn" label="Churn" />
<SortableListItem value="traffic" label="Traffic" />
</SortableList>
<SortableList label="Dashboard widgets" className="w-80 max-w-full">
<SortableListItem value="revenue" label="Revenue" />
<SortableListItem value="signups" label="Sign-ups" />
<SortableListItem value="churn" label="Churn" />
<SortableListItem value="traffic" label="Traffic" />
</SortableList>Examples
Dashboard widgets
import { SortableList, SortableListItem } from "@/components/ui/sortable-list"
<SortableList label="Dashboard widgets" className="w-80">
<SortableListItem value="revenue" label="Revenue" />
<SortableListItem value="signups" label="Sign-ups" />
<SortableListItem value="churn" label="Churn" />
<SortableListItem value="traffic" label="Traffic" />
</SortableList>Rows with content
Children replace the label in the row; the label still names the handle.
import { Badge } from "@/components/ui/badge"
import { SortableList, SortableListItem } from "@/components/ui/sortable-list"
<SortableList label="Priorities" className="w-96">
<SortableListItem value="billing" label="Fix billing emails">
<span className="flex-1 truncate">Fix billing emails</span>
<Badge variant="destructive">Urgent</Badge>
</SortableListItem>
<SortableListItem value="onboarding" label="Shorten onboarding">
<span className="flex-1 truncate">Shorten onboarding</span>
<Badge variant="info">Next</Badge>
</SortableListItem>
<SortableListItem value="export" label="CSV export">
<span className="flex-1 truncate">CSV export</span>
<Badge>Later</Badge>
</SortableListItem>
</SortableList>Small, with a locked row
A disabled row keeps its place in the order but has no working handle.
import { SortableList, SortableListItem } from "@/components/ui/sortable-list"
<SortableList label="Columns" size="sm" className="w-64">
<SortableListItem value="name" label="Name" disabled />
<SortableListItem value="email" label="Email" />
<SortableListItem value="role" label="Role" />
<SortableListItem value="joined" label="Joined" />
</SortableList>Do and don't
Do
- Save the new order as soon as onValueChange fires, so a reload keeps it.
- Keep each row to one or two lines, so a moving row is easy to follow.
- Put links and buttons in the content; only the handle starts a drag.
Don't
- Do not make rows reorderable when the order comes from a sort, such as by date. Use Table with sortable columns.
- Do not nest a SortableList inside another.
- Do not hide the handle; it is the only way to move a row on touch and with the keyboard.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| label required | string | - | - | On SortableList: names the list for screen readers, such as Dashboard widgets. On SortableListItem: the row's name in its handle and in the announcements; also the row's content when it has no children. |
| value | string[] on SortableList, string on SortableListItem | - | - | On SortableList: item values in their order, for a controlled list; items not named follow in written order. On SortableListItem: identifies the row in the order. Required on every item. |
| defaultValue | string[] | - | - | The first order, for an uncontrolled list. Without it the rows start in written order. |
| onValueChange | (value: string[]) => void | - | - | Called with the new order after a drop, and after each keyboard move. |
| size | enum | sm | md | md | Row height and padding: sm rows are at least size-element-md tall for dense panels, md rows at least size-element-xl for content with two lines. |
| disabled | boolean | - | false | Set on SortableList or SortableListItem. Turns the handles off; the content stays as it is. |
| labels | { handle?: string; instructions?: string; pickedUp?: string; moved?: string; dropped?: string; canceled?: string } | - | - | Text for the handle name, the keyboard instructions and the announcements, with {label}, {position} and {total} filled in. Defaults: EN `Move {label}`, `Picked up {label}. Position {position} of {total}.` and the like. Pass Vietnamese text on a Vietnamese page. |
Slots
| Name | Required | Description |
|---|---|---|
| content | no | Children of SortableListItem: any content after the handle, such as an icon, a name and a Badge. |
Anatomy
| Part | Description |
|---|---|
| root | The wrapper around the list and its live region. |
| list | The ul of rows. |
| sortableItem | One row: the handle, then the content. A surface with a border at rest; raised with a ring and a medium shadow while it moves. |
| handle | The 24px grip button at the start of the row. |
| icon | The grip icon inside the handle, 16px. |
| hitArea | The transparent ::before that grows the handle to size-target-min. |
| content | Any content after the handle; the label when there are no children. |
Accessibility
Role: list
| Keys | Action |
|---|---|
| Tab | Moves to the next row's handle. |
| Space / Enter | Picks up the focused row, or drops a picked-up row where it is. |
| ArrowUp / ArrowDown | Moves a picked-up row one place. ArrowLeft and ArrowRight do the same. |
| Home / End | Moves a picked-up row to the start or the end. |
| Escape | Cancels the move and puts the row back where it was picked up. |
- A ul named by label. Each handle is a button named EN `Move {label}`, VI `Di chuyển {label}`, and described by the keyboard instructions.
- A picked-up handle has aria-pressed="true" and the row sets data-dragging.
- Every pick up, move, drop and cancel is announced in a live region with the row's new position, such as EN `Revenue moved to position 2 of 4.`
- Moving focus away from a picked-up handle drops the row where it is.
- Pointer and touch drag start on the handle only, so the content can hold links and buttons. The handle reaches size-target-min on touch, through a transparent ::before.
Install
npx werewolf-ui add sortable-listRun npx werewolf-ui init once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See Installation for requirements and the alternative registry setup.