Search across all documentation pages
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
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=
Drag handle (only part of the item is draggable):
function SortableItemWithHandle({ id }: { id: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
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 and why is it required?DndContext is the root provider that manages all drag-and-drop stateDndContextonDragStart, onDragOver, and onDragEnd callbacksuseSortable differ from using useDraggable and useDroppable separately?useSortable 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 (
<
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
<DndContext modifiers={[restrictToVerticalAxis]}>
{/* ... */}
</DndContext>DragEndEvent handler in TypeScript?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;
}"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 itemsuseSortableApply attributes and listeners to the handle element, not the container.
UniqueIdentifier is string | number. Cast with as string if your IDs are always strings.