Draggable Sortable List
A drag-handle-driven list that reorders with a smooth layout swap.
- Category
- Patterns
- Added
- 2026-08-22
- Deps
- 2
- Updated
- 2026-09-01
- Wireframe
- Design
- Build
- Ship
Props
"use client";
import { useState } from "react";
import { Reorder, useDragControls } from "framer-motion";
import { GripVertical } from "lucide-react";
export function SortableList({ items: initial = ["Wireframe", "Design", "Build", "Ship"] }: { items?: string[] }) {
const [items, setItems] = useState(initial);
return (
<Reorder.Group axis="y" values={items} onReorder={setItems} className="flex w-72 flex-col gap-2">
{items.map((item) => (
<Row key={item} item={item} />
))}
</Reorder.Group>
);
}
function Row({ item }: { item: string }) {
const controls = useDragControls();
return (
<Reorder.Item
value={item}
dragListener={false}
dragControls={controls}
className="flex items-center gap-2 rounded-xl border border-neutral-200 bg-white px-3 py-2.5 shadow-sm"
>
<button
aria-label="Drag to reorder"
onPointerDown={(e) => controls.start(e)}
className="cursor-grab touch-none text-neutral-400 active:cursor-grabbing"
>
<GripVertical size={16} />
</button>
<span className="text-sm font-medium">{item}</span>
</Reorder.Item>
);
}
About this pattern
Framer Motion's Reorder.Group and Reorder.Item exist specifically so you do not hand-roll the onDragEnd index math yourself; they handle the drag gesture, the layout-animated swap as one item crosses over another, and committing the reordered array through onReorder, all internally. The part worth knowing before you copy this: each row is pulled into its own subcomponent purely so it can call its own useDragControls() hook, since calling that hook inside the parent's .map() callback would mean one component calling a hook a variable number of times depending on list length, a rules-of-hooks violation rather than a style preference. dragListener={false} on the item plus controls.start(event) on the handle's onPointerDown is what restricts dragging to the grip icon rather than the whole row, so row text stays selectable. Reach for this over a full library like dnd-kit when the interaction is a single flat list with a visible handle; dnd-kit earns its extra weight for multi-column drag targets or nested lists, which Reorder.Group does not handle. The real limitation: reordering is drag-only, with no keyboard path to move an item up or down the list.
Curator’s note
Each row is its own subcomponent purely so it can call its own useDragControls() — calling that hook inside the parent's .map() callback would call a hook a variable number of times from one component, the same rules-of-hooks trap Sticky Scroll Stack's useTransform hit.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Kanban Drag Columns
patternsA mini kanban board where cards drag both within and across columns.
Link Peek Preview
patternsAn index list that floats a velocity-tilted preview card beside the pointer.
Spotlight List Focus
patternsA list that dims and blurs every row except the one under the pointer.