Skip to content
WANCRAFT

B.70Cards & Containers

Sortable List

A drag-to-reorder list with a grip handle, lifted rows, layout animations and full keyboard reordering.

NewReactMotionTailwindadvanced

Overview

Sortable List reorders rows with Motion's Reorder primitives, so neighbours glide out of the way with layout animations while the dragged row lifts with a scale and a deeper shadow. Dragging starts from a grip handle (or the whole row) and can auto-scroll the nearest scrollable ancestor near its edges. The same handle is a real button for keyboard users: Space or Enter picks a row up, the arrow keys, Home and End move it, Space or Enter drops it and Escape restores the original order. Every pick-up, move, drop and cancel is announced in a polite live region, for example "Moved Design review to position 2 of 5". The list is fully controlled through items and onReorder, and renderItem lets you draw any row body.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/sortable-list.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/sortable-list.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/sortable-list.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { SortableList } from "@/components/ui/sortable-list";
import { useState } from "react";

export function SortableListExample() {
  const [items, setItems] = useState([
    { id: "brief", label: "Write the brief" },
    { id: "review", label: "Design review" },
    { id: "build", label: "Build the prototype" },
    { id: "ship", label: "Ship it" },
  ]);

  return (
    <div className="max-w-md">
      <SortableList items={items} onReorder={setItems} label="Tasks" />
    </div>
  );
}

03Props

PropTypeDefaultDescription
itemsT[] (T extends { id: string; label?: string; description?: ReactNode })—Items in their current order (controlled).
onReorder(items: T[]) => void—Called with the new order after every pointer or keyboard move.
renderItem(item: T, state: SortableItemState) => ReactNode—Renders a row body; receives index, total, dragging and grabbed.
getItemLabel(item: T) => stringitem.label ?? item.idText used for the handle name and announcements.
handleOnlybooleantrueOnly the grip handle starts a pointer drag.
showIndexbooleantrueShow the position number on each row.
density"comfortable" | "compact""comfortable"Row size.
liftnumber1.03Scale of a lifted row.
autoScrollbooleantrueScroll the nearest scrollable ancestor while dragging near its edges.
spring{ stiffness: number; damping: number; mass: number }{ stiffness: 520, damping: 40, mass: 1 }Spring for rows moving into place.
accentstring"#FF7448"Lifted outline, grabbed handle and focus ring colour.
labelstring"Sortable list"Accessible name of the list.
...propsHTMLAttributes<HTMLUListElement>—Native attributes are forwarded to the <ul>.

04Features

  • 01Motion Reorder with layout animations: neighbours glide aside while you drag
  • 02Grip handle (or whole-row) dragging with a lifted scale, outline and shadow
  • 03Optional auto-scroll of the nearest scrollable ancestor near its edges
  • 04Full keyboard reordering: pick up, move with arrows / Home / End, drop or cancel
  • 05Polite live-region announcements for every pick-up, move, drop and cancel
  • 06Controlled items + onReorder, and a renderItem slot for custom rows
motion · layoutmotion · springinput · draginput · keyboard

05Accessibility

  • Each handle is a <button> named "Reorder <item>" with aria-roledescription="sortable", described by visually hidden instructions.
  • aria-pressed reflects whether the row is picked up from the keyboard.
  • Moves are announced in a role="status" live region, e.g. "Moved Design review to position 2 of 5".
  • Escape restores the original order; moving focus away (Tab) drops the row where it is.
  • Focus stays on the moved row's handle after every keyboard move.
  • With prefers-reduced-motion, rows move into place without animation.

06Limitations

  • Single vertical list; dragging between lists is not supported.
  • When the list scrolls inside an element, add Motion's layoutScroll to that element so drag measurements account for its scroll position.