Reorderable
Drag and drop reorderable list component with visual feedback and smooth animations.
Installation
npx shadcn@latest add https://spoke.georgedrury.co.uk/r/reorderableLineage
Spoke
Anatomy
import {
ReorderableList,
ReorderableItem,
ReorderableTrigger,
ReorderableSeparator,
} from "@/components/ui/reorderable"
export default () => {
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3"])
return (
<ReorderableList items={items} onReorder={setItems}>
{items.map((item, index) => (
<>
<ReorderableSeparator index={index} />
<ReorderableItem key={index} index={index}>
<ReorderableTrigger />
{item}
</ReorderableItem>
</>
))}
<ReorderableSeparator index={items.length} />
</ReorderableList>
)
}Usage
Basic Example
"use client"
import { useState } from "react"
import {
ReorderableList,
ReorderableItem,
ReorderableTrigger,
ReorderableSeparator,
} from "@/components/ui/reorderable"
export default function ReorderableDemo() {
const [tasks, setTasks] = useState([
"Design landing page",
"Implement authentication",
"Write documentation",
])
return (
<ReorderableList items={tasks} onReorder={setTasks} className="space-y-2">
{tasks.map((task, index) => (
<>
<ReorderableSeparator key={`sep-${index}`} index={index} />
<ReorderableItem
key={task}
index={index}
className="flex items-center gap-2 p-4 bg-card border rounded-lg"
>
<ReorderableTrigger className="cursor-move">⋮⋮</ReorderableTrigger>
<span>{task}</span>
</ReorderableItem>
</>
))}
<ReorderableSeparator index={tasks.length} />
</ReorderableList>
)
}With Complex Items
"use client"
import { useState } from "react"
import { GripVertical } from "lucide-react"
import {
ReorderableList,
ReorderableItem,
ReorderableTrigger,
ReorderableSeparator,
} from "@/components/ui/reorderable"
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
interface Project {
id: string
title: string
description: string
}
export default function ProjectList() {
const [projects, setProjects] = useState<Project[]>([
{ id: "1", title: "Website Redesign", description: "Modernize the company website" },
{ id: "2", title: "Mobile App", description: "Build iOS and Android apps" },
{ id: "3", title: "API Integration", description: "Connect third-party services" },
])
return (
<ReorderableList items={projects} onReorder={setProjects} className="space-y-2">
{projects.map((project, index) => (
<>
<ReorderableSeparator key={`sep-${index}`} index={index} />
<ReorderableItem key={project.id} index={index}>
<Card className="cursor-default">
<CardHeader className="flex flex-row items-center gap-3">
<ReorderableTrigger asChild>
<button className="cursor-move p-1 hover:bg-accent rounded">
<GripVertical className="h-5 w-5 text-muted-foreground" />
</button>
</ReorderableTrigger>
<div>
<CardTitle>{project.title}</CardTitle>
<CardDescription>{project.description}</CardDescription>
</div>
</CardHeader>
</Card>
</ReorderableItem>
</>
))}
<ReorderableSeparator index={projects.length} />
</ReorderableList>
)
}Components
ReorderableList
The root container component that manages drag and drop state.
Props:
items: T[]- Array of items to reorderonReorder: (items: T[]) => void- Callback fired when items are reorderedclassName?: string- Optional CSS classes- All standard div props
ReorderableItem
Individual draggable item in the list.
Props:
index: number- Index of the item in the items arrayasChild?: boolean- Use Radix Slot to merge props with child elementclassName?: string- Optional CSS classes- All standard div props
ReorderableTrigger
The drag handle for an item. Can be placed anywhere within a ReorderableItem.
Props:
asChild?: boolean- Use Radix Slot to merge props with child elementclassName?: string- Optional CSS classes (includescursor-moveby default)- All standard div props
ReorderableSeparator
Visual drop indicator between items. Place one before each item and one after the last item.
Props:
index: number- Position where the drop indicator should appearasChild?: boolean- Use Radix Slot to merge props with child elementclassName?: string- Optional CSS classes- All standard div props
Spoke utility classes
| Class | Fallback chain | Description |
|---|---|---|
--reorderable-separator | var(--primary) | Color for drop indicator between items |
Data Attributes
The component provides several data attributes for styling different states:
data-reorderable-list- Applied to the list containerdata-reorderable-item- Applied to each itemdata-reorderable-trigger- Applied to drag handlesdata-reorderable-separator- Applied to drop indicatorsdata-dragging="true"- Applied to the item being draggeddata-drag-active="true"- Applied to all items when draggingdata-drop-target="true"- Applied to items at the drop position
Notes
- Uses native HTML5 drag and drop API
- Supports keyboard navigation (drag handles are focusable)
- Items must have stable keys for proper React reconciliation
- The
onReordercallback is called during drag, not just on drop - Separators provide visual feedback for drop position