dnd-kit is the modern, accessible drag-and-drop toolkit for React. It supports sortable lists, kanban boards, and complex multi-container drag interactions.
Step 1: Install dnd-kit
pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
Step 2: Basic Sortable List
"use client";
import { useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface Item {
id: string;
title: string;
}
function SortableItem({ item }: { item: Item }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
className={`flex items-center gap-3 rounded-lg border bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 ${
isDragging ? "z-50 opacity-50 shadow-lg" : ""
}`}
>
<button
{...attributes}
{...listeners}
className="cursor-grab text-gray-400 active:cursor-grabbing"
aria-label={`Drag ${item.title}`}
>
⠿
</button>
<span className="text-sm font-medium">{item.title}</span>
</div>
);
}
export function SortableList() {
const [items, setItems] = useState<Item[]>([
{ id: "1", title: "Design homepage" },
{ id: "2", title: "Build contact form" },
{ id: "3", title: "Add SEO meta tags" },
{ id: "4", title: "Deploy to production" },
]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) return;
setItems((prev) => {
const oldIndex = prev.findIndex((i) => i.id === active.id);
const newIndex = prev.findIndex((i) => i.id === over.id);
return arrayMove(prev, oldIndex, newIndex);
});
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{items.map((item) => (
<SortableItem key={item.id} item={item} />
))}
</div>
</SortableContext>
</DndContext>
);
}
Step 3: Drag Overlay
Add a visual overlay that follows the cursor:
import { DragOverlay } from "@dnd-kit/core";
import { useState } from "react";
const [activeId, setActiveId] = useState<string | null>(null);
const activeItem = items.find((i) => i.id === activeId);
<DndContext
onDragStart={(event) => setActiveId(String(event.active.id))}
onDragEnd={(event) => {
setActiveId(null);
handleDragEnd(event);
}}
onDragCancel={() => setActiveId(null)}
>
<SortableContext items={items}>
{items.map((item) => (
<SortableItem key={item.id} item={item} />
))}
</SortableContext>
<DragOverlay>
{activeItem ? (
<div className="rounded-lg border bg-white p-4 shadow-xl">
{activeItem.title}
</div>
) : null}
</DragOverlay>
</DndContext>
Step 4: Kanban Board
"use client";
import { useState } from "react";
import {
DndContext,
DragOverlay,
closestCorners,
type DragEndEvent,
type DragOverEvent,
type DragStartEvent,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
interface Task {
id: string;
title: string;
column: string;
}
const COLUMNS = ["To Do", "In Progress", "Done"];
export function KanbanBoard() {
const [tasks, setTasks] = useState<Task[]>([
{ id: "1", title: "Research competitors", column: "To Do" },
{ id: "2", title: "Design wireframes", column: "To Do" },
{ id: "3", title: "Build prototype", column: "In Progress" },
{ id: "4", title: "Write copy", column: "Done" },
]);
const [activeTask, setActiveTask] = useState<Task | null>(null);
function handleDragStart(event: DragStartEvent) {
const task = tasks.find((t) => t.id === event.active.id);
setActiveTask(task || null);
}
function handleDragOver(event: DragOverEvent) {
const { active, over } = event;
if (!over) return;
const activeTask = tasks.find((t) => t.id === active.id);
const overColumn = COLUMNS.includes(String(over.id))
? String(over.id)
: tasks.find((t) => t.id === over.id)?.column;
if (activeTask && overColumn && activeTask.column !== overColumn) {
setTasks((prev) =>
prev.map((t) =>
t.id === active.id ? { ...t, column: overColumn } : t
)
);
}
}
function handleDragEnd(event: DragEndEvent) {
setActiveTask(null);
// Persist order to database here
}
return (
<DndContext
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="grid grid-cols-3 gap-4">
{COLUMNS.map((column) => {
const columnTasks = tasks.filter((t) => t.column === column);
return (
<div
key={column}
className="rounded-xl bg-gray-100 p-4 dark:bg-gray-800"
>
<h3 className="mb-3 text-sm font-semibold">{column}</h3>
<SortableContext
items={columnTasks}
strategy={verticalListSortingStrategy}
>
<div className="space-y-2">
{columnTasks.map((task) => (
<SortableItem key={task.id} item={task} />
))}
</div>
</SortableContext>
</div>
);
})}
</div>
<DragOverlay>
{activeTask ? (
<div className="rounded-lg border bg-white p-3 shadow-xl">
{activeTask.title}
</div>
) : null}
</DragOverlay>
</DndContext>
);
}
Step 5: Grid Layout Sorting
import { rectSortingStrategy } from "@dnd-kit/sortable";
<SortableContext items={items} strategy={rectSortingStrategy}>
<div className="grid grid-cols-3 gap-4">
{items.map((item) => (
<SortableGridItem key={item.id} item={item} />
))}
</div>
</SortableContext>
Step 6: Restrict Drag Axis
import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis, restrictToParentElement]}>
{/* content */}
</DndContext>
Step 7: Persist Order
async function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) return;
const updated = arrayMove(/* ... */);
setItems(updated);
// Persist to database
await fetch("/api/reorder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: updated.map((item, index) => ({
id: item.id,
order: index,
})),
}),
});
}
Need Interactive UI Features?
We build web applications with drag-and-drop, kanban boards, and other advanced interactive features. Contact us for a consultation.