Skip to content

Pro pre-sale is open. Installation opens January 2027. See Pro

Components

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.

  • npm: 1 direct, 2 total
  • registry: 2 direct, 2 total

Preview

  • Revenue
  • Sign-ups
  • Churn
  • Traffic

Props

  • label
  • size
  • disabled
  • badges
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.

    • Revenue
    • Sign-ups
    • Churn
    • Traffic

    sm

    • Revenue
    • Sign-ups
    • Churn
    • Traffic

    mddefault

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

  • Revenue
  • Sign-ups
  • Churn
  • Traffic
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.

  • Fix billing emailsUrgent
  • Shorten onboardingNext
  • CSV exportLater
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.

  • Name
  • Email
  • Role
  • Joined
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

NameTypeValuesDefaultDescription
label requiredstring--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.
valuestring[] 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.
defaultValuestring[]--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.
sizeenumsm | mdmdRow 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.
disabledboolean-falseSet 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

NameRequiredDescription
contentnoChildren of SortableListItem: any content after the handle, such as an icon, a name and a Badge.

Anatomy

PartDescription
rootThe wrapper around the list and its live region.
listThe ul of rows.
sortableItemOne row: the handle, then the content. A surface with a border at rest; raised with a ring and a medium shadow while it moves.
handleThe 24px grip button at the start of the row.
iconThe grip icon inside the handle, 16px.
hitAreaThe transparent ::before that grows the handle to size-target-min.
contentAny content after the handle; the label when there are no children.

Accessibility

Role: list

KeysAction
TabMoves to the next row's handle.
Space / EnterPicks up the focused row, or drops a picked-up row where it is.
ArrowUp / ArrowDownMoves a picked-up row one place. ArrowLeft and ArrowRight do the same.
Home / EndMoves a picked-up row to the start or the end.
EscapeCancels 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-list

Run 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.