Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities"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";
function SortableItem({ id }: { id: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="p-3 bg-white border rounded shadow-sm cursor-grab"
>
{id}
</div>
);
}
export default function SortableList() {
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3"]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (over && active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id as string);
const newIndex = items.indexOf(over.id as string);
return arrayMove(items, oldIndex, newIndex);
});
}
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{items.map((id) => (
<SortableItem key={id} id={id} />
))}
</div>
</SortableContext>
</DndContext>
);
}When to reach for this: You need drag-and-drop reordering, sortable lists, kanban boards, or draggable UI elements with keyboard accessibility.
// app/components/KanbanBoard.tsx
"use client";
import { useState } from "react";
import {
DndContext,
DragOverlay,
closestCorners,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragStartEvent,
type DragEndEvent,
type DragOverEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useDroppable } from "@dnd-kit/core";
interface Task {
id: string;
title: string;
}
type Columns = Record<string, Task[]>;
function TaskCard({ 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,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="p-3 bg-white border rounded shadow-sm cursor-grab"
>
{task.title}
</div>
);
}
function Column({ id, tasks }: { id: string; tasks: Task[] }) {
const { setNodeRef } = useDroppable({ id });
return (
<div ref={setNodeRef} className="w-72 bg-gray-100 rounded-lg p-4">
<h3 className="font-bold mb-3 capitalize">{id}</h3>
<SortableContext
items={tasks.map((t) => t.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-2 min-h-[100px]">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
</SortableContext>
</div>
);
}
export default function KanbanBoard() {
const [columns, setColumns] = useState<Columns>({
todo: [
{ id: "1", title: "Design mockups" },
{ id: "2", title: "Write tests" },
],
"in-progress": [{ id: "3", title: "Build API" }],
done: [{ id: "4", title: "Setup repo" }],
});
const [activeTask, setActiveTask] = useState<Task | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function findColumn(taskId: string): string | undefined {
for (const [columnId, tasks] of Object.entries(columns)) {
if (tasks.some((t) => t.id === taskId)) return columnId;
}
// Check if taskId is a column id (for dropping on empty columns)
if (columns[taskId]) return taskId;
return undefined;
}
function handleDragStart(event: DragStartEvent) {
const col = findColumn(event.active.id as string);
if (col) {
const task = columns[col].find((t) => t.id === event.active.id);
setActiveTask(task ?? null);
}
}
function handleDragOver(event: DragOverEvent) {
const { active, over } = event;
if (!over) return;
const activeCol = findColumn(active.id as string);
const overCol = findColumn(over.id as string);
if (!activeCol || !overCol || activeCol === overCol) return;
setColumns((prev) => {
const activeItems = [...prev[activeCol]];
const overItems = [...prev[overCol]];
const activeIndex = activeItems.findIndex((t) => t.id === active.id);
const [movedTask] = activeItems.splice(activeIndex, 1);
const overIndex = overItems.findIndex((t) => t.id === over.id);
overItems.splice(overIndex >= 0 ? overIndex : overItems.length, 0, movedTask);
return { ...prev, [activeCol]: activeItems, [overCol]: overItems };
});
}
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
setActiveTask(null);
if (!over) return;
const activeCol = findColumn(active.id as string);
const overCol = findColumn(over.id as string);
if (!activeCol || !overCol || activeCol !== overCol) return;
const items = columns[activeCol];
const oldIndex = items.findIndex((t) => t.id === active.id);
const newIndex = items.findIndex((t) => t.id === over.id);
if (oldIndex !== newIndex) {
setColumns((prev) => ({
...prev,
[activeCol]: arrayMove(prev[activeCol], oldIndex, newIndex),
}));
}
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 p-6">
{Object.entries(columns).map(([id, tasks]) => (
<Column key={id} id={id} tasks={tasks} />
))}
</div>
<DragOverlay>
{activeTask ? (
<div className="p-3 bg-white border rounded shadow-lg rotate-3">
{activeTask.title}
</div>
) : null}
</DragOverlay>
</DndContext>
);
}What this demonstrates:
DragOverlay for a custom drag previewonDragOveronDragEndKeyboardSensorDndContext is the root provider that manages drag state, sensors, and collision detectionPointerSensor, KeyboardSensor, TouchSensor) determine how drag interactions are initiated and trackeduseSortable combines useDraggable and useDroppable into a single hook for sortable itemsSortableContext tracks the order of items and provides the sorting strategy (vertical, horizontal, or grid)closestCenter, closestCorners, rectIntersection) determine which droppable target is active during a dragDragOverlay renders a floating element that follows the cursor, separate from the DOM position of the dragged itemarrayMove is a utility that reorders an array by moving an element from one index to anotherHorizontal sortable list:
import { horizontalListSortingStrategy } from "@dnd-kit/sortable";
<SortableContext items={items} strategy={horizontalListSortingStrategy}>
<div className="flex gap-2">
{items.map((id) => (
<SortableItem key={id} id={id} />
))}
</div>
</SortableContext>Drag handle (only part of the item is draggable):
function SortableItemWithHandle({ id }: { id: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} className="flex items-center gap-2 p-3 border rounded">
<button {...attributes} {...listeners} className="cursor-grab">
☰
</button>
<span>{id}</span>
</div>
);
}Restricting drag axis:
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis]}>
{/* ... */}
</DndContext>DragEndEvent, DragStartEvent, DragOverEvent provide typed active and over objectsactive.id and over.id are UniqueIdentifier (string or number)as string when your IDs are known to be stringsuseSortable returns a typed object; destructure only the properties you needimport type { DragEndEvent, UniqueIdentifier } from "@dnd-kit/core";
function handleDragEnd(event: DragEndEvent) {
const activeId: UniqueIdentifier = event.active.id;
const overId: UniqueIdentifier | undefined = event.over?.id;
}Items jump on drag start - The dragged item's space collapses, causing layout shift. Fix: Use DragOverlay for the visual preview and keep the original item in place with reduced opacity.
Accidental drags on click - Single clicks trigger drag events. Fix: Add activationConstraint: { distance: 8 } to PointerSensor to require 8px of movement before activating.
IDs must be unique across all containers - If two items in different columns share an ID, dnd-kit cannot determine the correct container. Fix: Use globally unique IDs (e.g., UUIDs) for all draggable items.
SSR hydration mismatch - dnd-kit uses DOM measurements that don't exist on the server. Fix: Ensure the component is marked "use client" and wrap in a client-only boundary if needed.
Performance with many items - Rendering hundreds of sortable items causes lag. Fix: Use virtualization (e.g., react-virtual) and only wrap visible items in useSortable.
| Library | Best For | Trade-off |
|---|---|---|
| dnd-kit | Modern, accessible, composable DnD | Requires more setup for complex cases |
| react-beautiful-dnd | Simple sortable lists | Deprecated, no longer maintained |
| react-dnd | Complex custom DnD interactions | Steeper learning curve, HTML5 backend limitations |
| @hello-pangea/dnd | Drop-in react-beautiful-dnd replacement | Fork, community-maintained |
| Native HTML drag | Simple drag without libraries | No keyboard accessibility, limited control |
@dnd-kit/core -- provides DndContext, sensors, and collision detection@dnd-kit/sortable -- provides SortableContext, useSortable, and arrayMove@dnd-kit/utilities -- provides CSS.Transform.toString for applying transformsnpm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilitiesDndContext is the root provider that manages all drag-and-drop stateDndContextonDragStart, onDragOver, and onDragEnd callbacksuseSortable combines both hooks into one for items that are both draggable and droppableuseDraggable + useDroppable separately only when items are draggable but not sortableDragOverlay to render a floating preview that follows the cursoropacity: 0.5 while draggingDragOverlay renders outside the normal DOM flow and avoids layout shiftsconst sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
})
);The distance: 8 constraint requires 8px of movement before a drag activates.
function SortableItem({ id }: { id: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
return (
<div ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }}>
<button {...attributes} {...listeners}>Drag handle</button>
<span>{id}</span>
</div>
);
}Apply attributes and listeners to the handle element, not the container.
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis]}>
{/* ... */}
</DndContext>import type { DragEndEvent, UniqueIdentifier } from "@dnd-kit/core";
function handleDragEnd(event: DragEndEvent) {
const activeId: UniqueIdentifier = event.active.id;
const overId: UniqueIdentifier | undefined = event.over?.id;
}UniqueIdentifier is string | number. Cast with as string if your IDs are always strings.
"use client"closestCenter -- best for single-container sortable listsclosestCorners -- best for multi-container setups (kanban boards)rectIntersection -- triggers when rectangles overlap, good for grid layoutsclosestCenter is the most common defaultuseSortable hooks causes noticeable lag@tanstack/react-virtual) to render only visible itemsuseSortableReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥