Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Sortable List

Reorderable rows with pointer drag, keyboard move mode, a lossless row menu, and server-refusable moves — controlled; the host owns the order.

Status
stable
Since
0.4.0
Accessibility pattern
keyboard-first drag alternative

Last updated

Lead
Qualified
Proposal
WonLocked

Drag the handle, press Space on it for keyboard move mode, or use the row menu — every path reaches every order.

Install

Add Sortable List from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/sortable-list

The same command installs the registry items it composes: @vegastack/drag-item, @vegastack/dropdown-menu, @vegastack/icon-button, @vegastack/item, @vegastack/use-drag-reorder.

Usage

import { SortableList } from "@/components/ui/sortable-list";

<SortableList
  aria-label="Pipeline stages"
  items={stages}
  renderItem={(stage) => <span>{stage.label}</span>}
  onReorder={({ id, to }) => moveStage(id, to.index)}
/>;

SortableList is the single-list consumer of use-drag-reorder (the system's one drag-engine seam). It is controlled: items arrive in display order, onReorder requests a move, and the host applies — or refuses — it.

Scope

BehaviourWhere it lives
The persisted orderHost — onReorder is a request; the app owns storage
SelectionNot here, deliberately: reordering + multi-select on one surface makes drag intent ambiguous — DataList owns selection
Cross-container movesBoard
Virtualized orderingdata-grid territory

Examples

Server-gated moves

Return a promise from onReorder: the moved row shimmers while the write is in flight; a rejection announces the snap-back — the host never applied the move, so the order simply stays.

Verified domains
SSO providers
Webhooks

Moving “Webhooks” is refused by the host: the row shimmers while pending, then snaps back and announces the rejection.

The menu path

Every enabled row carries a "Move …" menu — Move up / Move down / Move to top / Move to bottom — so the whole ordering is reachable without a pointer and without entering keyboard move mode. It runs the same onReorder callback as a drag, so the host cannot tell the paths apart.

The drop-edge hairline, the lift dim and the pending shimmer come from the shared drag-item recipe — the same one Board uses, installed once as its own registry item.

Overview
Members
Billing
Audit log

Open a row’s menu to move it without dragging — the same reorder callback runs, so the host cannot tell the paths apart.

API Reference

PropTypeDefaultDescription
items*readonly SortableListItem[]Rows in display order — controlled; the host re-orders on onReorder.
onReorder*(move: DragReorderMove) => void | Promise<void>Apply a requested move. Return a promise for server-gated ordering — the moved row shows the pending shimmer and a rejection announces + snaps back (the host never applied it).
renderItem*(item: SortableListItem) => React.ReactNodeRender a row's content (everything except the handle and menu).
aria-labelstring"Sortable list"Accessible name for the list.
classNamestringExtra classes for the list root.
disabledbooleanfalseDisable all reordering (rows render without handles or menus).
refReact.Ref<HTMLDivElement>Ref forwarded to the list root (data-slot="sortable-list").

Data attributes and CSS variables on SortableList

AttributeValues
data-drag-itemmirrors a prop or state value
data-drag-pendingmirrors a prop or state value
data-draggingmirrors a prop or state value
data-drop-containermirrors a prop or state value
data-drop-edgemirrors a prop or state value
data-drop-overmirrors a prop or state value
data-slot"sortable-list" | "sortable-list-item"

Item

PropTypeDefaultDescription
id*stringStable id — the identity onReorder moves.
disabledbooleanfalseExclude this row from reordering (its handle and menu disable).
labelstringAccessible name for the row's handle and menu ("Reorder {label}"). Falls back to the id.

Accessibility

  • Every reorder is reachable three ways: pointer drag on the handle, the keyboard move mode (Space lifts, arrows commit one announced step at a time, Escape ends), and the row's Move menu — the required lossless path on touch and for AT.
  • Each step announces through a polite live region ("Moved to position 2 of 3"); a refused move announces "Move rejected — position restored".
  • Handles and menu triggers are named per row ("Reorder Alpha", "Move Alpha"); drop position shows as a 2px indicator line, and a lifted row dims — flat by doctrine, never a shadow.
KeyAction
Space / EnterToggle move mode on the focused handle.
/ Commit a one-step move (in move mode).
EscapeEnd move mode.
ContractStates tested
Behaviouridle, dragging, move-mode, pending, rejected, disabled, item-disabled
Accessibilitykeyboard, labeled, status-announcement, semantic-html
Visualdefault, dragging, drop-edge, pending, disabled

Do / Don't

Do
Keep the list controlled and refuse bad orders by rejecting the onReorder promise — the snap-back and announcement are built in.
Don't
Add selection checkboxes to sortable rows — ambiguous drag intent; compose DataList when rows need selecting.

On this page