dnd kit provides accessible drag and drop for React — it ships without HTML5 drag and drop API limitations, supports touch and pointer events, and includes ARIA announcements for keyboard users. DndContext wraps the interactive area and provides onDragStart, onDragOver, and onDragEnd callbacks. useDraggable({ id }) makes any element draggable; useDroppable({ id }) defines drop targets. SortableContext manages ordered lists; useSortable({ id }) combines drag and drop in one hook for list items. DragOverlay renders a portal overlay for smooth visual feedback during drag. closestCorners and closestCenter are the standard collision detection strategies. Sensors handle mouse, touch, pointer, and keyboard input. Claude Code generates dnd kit implementations for sortable lists, kanban boards, file uploaders, and multi-container drag-and-drop interfaces.
CLAUDE.md for dnd kit
## dnd kit Stack
- Version: @dnd-kit/core >= 6.3, @dnd-kit/sortable >= 8.0, @dnd-kit/utilities
- DndContext: wrap all draggable/droppable elements — onDragStart/Over/End callbacks
- Sortable: <SortableContext items={ids} strategy={verticalListSortingStrategy}>
- useSortable: const { setNodeRef, attributes, listeners, transform, isDragging } = useSortable({ id })
- Transform: style={{ transform: CSS.Translate.toString(transform) }} — avoid scale during sort
- Overlay: <DragOverlay> renders dragged clone as portal — prevents layout shift
- arrayMove: from @dnd-kit/sortable — reorder array: arrayMove(items, oldIndex, newIndex)
- Collision: closestCorners for kanban; closestCenter for single lists
Sortable List
// components/dnd/SortableOrdersList.tsx — vertical sortable list
"use client"
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
type DragEndEvent,
type DragStartEvent,
} from "@dnd-kit/core"
import {
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
arrayMove,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { useState } from "react"
import { GripVertical } from "lucide-react"
interface Task {
id: string
title: string
priority: "low" | "medium" | "high"
}
function SortableTaskItem({ task }: { task: Task }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: task.id })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
const priorityColors = {
low: "bg-slate-100 text-slate-700",
medium: "bg-amber-100 text-amber-700",
high: "bg-red-100 text-red-700",
}
return (
<div
ref={setNodeRef}
style={style}
className="flex items-center gap-3 rounded-lg border bg-white p-3 shadow-sm"
>
<button
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground touch-none"
aria-label="Drag to reorder"
>
<GripVertical className="h-4 w-4" />
</button>
<span className="flex-1 text-sm font-medium">{task.title}</span>
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${priorityColors[task.priority]}`}>
{task.priority}
</span>
</div>
)
}
function TaskCardOverlay({ task }: { task: Task }) {
return (
<div className="flex items-center gap-3 rounded-lg border bg-white p-3 shadow-xl ring-2 ring-primary">
<GripVertical className="h-4 w-4 text-muted-foreground" />
<span className="flex-1 text-sm font-medium">{task.title}</span>
</div>
)
}
export function SortableTasksList({
initialTasks,
onReorder,
}: {
initialTasks: Task[]
onReorder: (tasks: Task[]) => void
}) {
const [tasks, setTasks] = useState(initialTasks)
const [activeTask, setActiveTask] = useState<Task | null>(null)
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 }, // Require 8px movement before drag starts
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
function handleDragStart({ active }: DragStartEvent) {
setActiveTask(tasks.find(t => t.id === active.id) ?? null)
}
function handleDragEnd({ active, over }: DragEndEvent) {
setActiveTask(null)
if (!over || active.id === over.id) return
setTasks(current => {
const oldIndex = current.findIndex(t => t.id === active.id)
const newIndex = current.findIndex(t => t.id === over.id)
const reordered = arrayMove(current, oldIndex, newIndex)
onReorder(reordered)
return reordered
})
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{tasks.map(task => (
<SortableTaskItem key={task.id} task={task} />
))}
</div>
</SortableContext>
<DragOverlay>
{activeTask && <TaskCardOverlay task={activeTask} />}
</DragOverlay>
</DndContext>
)
}
Kanban Board — Multi-Container
// components/dnd/KanbanBoard.tsx — drag between columns
"use client"
import {
DndContext,
DragOverlay,
closestCorners,
useSensor,
useSensors,
PointerSensor,
KeyboardSensor,
type DragEndEvent,
type DragOverEvent,
type DragStartEvent,
} from "@dnd-kit/core"
import {
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
arrayMove,
} from "@dnd-kit/sortable"
import { useDroppable } from "@dnd-kit/core"
import { CSS } from "@dnd-kit/utilities"
import { useState } from "react"
type Status = "todo" | "in_progress" | "done"
interface Card {
id: string
title: string
status: Status
}
const COLUMNS: { id: Status; label: string }[] = [
{ id: "todo", label: "To Do" },
{ id: "in_progress", label: "In Progress" },
{ id: "done", label: "Done" },
]
function KanbanCard({ card }: { card: Card }) {
const { setNodeRef, attributes, listeners, transform, transition, isDragging } =
useSortable({ id: card.id, data: { card, type: "card" } })
return (
<div
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0 : 1,
}}
className="rounded-lg bg-white border p-3 shadow-sm cursor-grab active:cursor-grabbing"
{...attributes}
{...listeners}
>
<p className="text-sm font-medium">{card.title}</p>
</div>
)
}
function KanbanColumn({ status, cards, label }: { status: Status; cards: Card[]; label: string }) {
const { setNodeRef, isOver } = useDroppable({ id: status })
return (
<div
ref={setNodeRef}
className={`rounded-xl p-3 min-h-[400px] w-72 transition-colors ${
isOver ? "bg-blue-50 ring-2 ring-blue-200" : "bg-muted"
}`}
>
<h3 className="font-semibold text-sm mb-3 flex items-center justify-between">
{label}
<span className="bg-background rounded-full px-2 py-0.5 text-xs">{cards.length}</span>
</h3>
<SortableContext items={cards.map(c => c.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{cards.map(card => (
<KanbanCard key={card.id} card={card} />
))}
</div>
</SortableContext>
</div>
)
}
export function KanbanBoard({ initialCards }: { initialCards: Card[] }) {
const [cards, setCards] = useState(initialCards)
const [activeCard, setActiveCard] = useState<Card | null>(null)
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)
function handleDragStart({ active }: DragStartEvent) {
setActiveCard(cards.find(c => c.id === active.id) ?? null)
}
function handleDragOver({ active, over }: DragOverEvent) {
if (!over) return
const activeCard = cards.find(c => c.id === active.id)!
const overStatus = (COLUMNS.find(col => col.id === over.id)?.id ??
cards.find(c => c.id === over.id)?.status) as Status | undefined
if (overStatus && activeCard.status !== overStatus) {
setCards(prev =>
prev.map(c => c.id === active.id ? { ...c, status: overStatus } : c)
)
}
}
function handleDragEnd({ active, over }: DragEndEvent) {
setActiveCard(null)
if (!over || active.id === over.id) return
const overCard = cards.find(c => c.id === over.id)
if (!overCard) return
setCards(prev => {
const oldIndex = prev.findIndex(c => c.id === active.id)
const newIndex = prev.findIndex(c => c.id === over.id)
return arrayMove(prev, oldIndex, newIndex)
})
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 overflow-x-auto pb-4">
{COLUMNS.map(col => (
<KanbanColumn
key={col.id}
status={col.id}
label={col.label}
cards={cards.filter(c => c.status === col.id)}
/>
))}
</div>
<DragOverlay>
{activeCard && (
<div className="rounded-lg bg-white border p-3 shadow-2xl ring-2 ring-primary rotate-3 w-72">
<p className="text-sm font-medium">{activeCard.title}</p>
</div>
)}
</DragOverlay>
</DndContext>
)
}
For the React Beautiful DnD alternative that is now unmaintained (archived by Atlassian) but was the previous standard for kanban-style drag-and-drop with opinionated beautiful animations — dnd kit is the recommended replacement with better maintained code and accessibility support, see the migration guide patterns. For native HTML5 drag and drop when browser compatibility is the priority and complex touch support or accessibility aren’t required — useful for simple file drop targets or basic draggable elements without the full dnd kit overhead, see the file upload patterns guide. The Claude Skills 360 bundle includes dnd kit skill sets covering sortable lists, kanban boards, and multi-container drag. Start with the free tier to try drag-and-drop implementation generation.