dnd-kit is a modern, lightweight drag-and-drop toolkit for React that prioritizes accessibility, performance, and extensibility. Unlike older libraries such as react-beautiful-dnd or react-dnd, dnd-kit was designed from the ground up with hooks-based APIs, first-class keyboard and screen reader support, and a modular architecture where you only import what you need. The library weighs roughly 10KB gzipped for core functionality and avoids the global state management overhead of legacy approaches.
The library is organized into several packages: @dnd-kit/core provides the foundational drag-and-drop primitives (sensors, collision detection, context providers), @dnd-kit/sortable adds optimized sorting interactions with animated reordering, and @dnd-kit/utilities supplies transform helpers and CSS utilities. This modular design means a simple sortable list only loads sorting logic, while a complex multi-container Kanban board can layer on additional collision algorithms and custom sensors.
The core design philosophy of dnd-kit treats drag-and-drop as an interaction pattern rather than a visual effect. Draggable elements, droppable containers, and the drag overlay are separate concerns that compose together. This separation enables sophisticated UIs where the visual feedback during a drag (the overlay) is completely independent from the dragged element’s DOM position, avoiding the layout thrashing and z-index issues that plague simpler implementations.
The DndContext component is the root provider that manages all drag-and-drop state. It tracks which element is being dragged, which droppable containers are active, and dispatches events (onDragStart, onDragOver, onDragEnd, onDragCancel) to the application. Every draggable and droppable element must be a descendant of a DndContext. Multiple independent DndContext providers can coexist on the same page for isolated drag-and-drop regions.
Sensors abstract the input method that initiates and controls a drag. The PointerSensor handles mouse and touch input with configurable activation distance (preventing accidental drags from clicks). The KeyboardSensor enables full drag-and-drop via arrow keys, Enter, and Escape. Custom sensors can be created for specialized inputs like gamepads or gesture-based interfaces. Sensors are composed as an array and evaluated in order, with the first matching sensor taking control.
dnd-kit ships with several collision detection strategies: closestCenter (measures distance between drag overlay center and droppable centers), closestCorners (uses corner distances for better accuracy with differently-sized containers), rectIntersection (detects overlapping bounding rectangles), and pointerWithin (checks if the pointer itself is inside a droppable). For complex layouts, a custom collision detection function can combine multiple strategies or add application-specific logic.
The DragOverlay component renders a visual representation of the dragged item that follows the cursor, separate from the original element’s DOM position. This is critical for avoiding layout reflow during drags, enabling smooth animations, and supporting dragging between containers where the original element’s CSS context (overflow, transforms, z-index) would otherwise clip or misposition the drag preview. The overlay renders in a portal by default, ensuring it appears above all other content.
The @dnd-kit/sortable package provides the SortableContext and useSortable hook that handle the common case of reorderable lists. SortableContext wraps a list of items and tracks their order. useSortable combines useDraggable and useDroppable into a single hook that manages both roles, applies CSS transforms for smooth animated reordering, and exposes transition styles. Different sorting strategies (verticalListSortingStrategy, horizontalListSortingStrategy, rectSortingStrategy) optimize performance for specific layouts.
At the lowest level, useDraggable returns props and refs that make an element draggable, while useDroppable marks an element as a valid drop target. The draggable hook provides attributes (ARIA roles), listeners (pointer/keyboard event handlers), setNodeRef (ref callback for DOM measurement), and transform (the current drag offset as { x, y }). The droppable hook provides setNodeRef and isOver (whether a draggable is currently over this target).
import { DndContext, useDraggable, useDroppable, DragEndEvent } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
const DraggableItem: React.FC<{ id: string; children: React.ReactNode }> = ({
id,
children,
}) => {
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
id,
});
const style: React.CSSProperties = {
transform: CSS.Translate.toString(transform),
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
};
return (
<div ref={setNodeRef} style={style} {...listeners} {...attributes}>
{children}
</div>
);
};
const DroppableZone: React.FC<{ id: string; children: React.ReactNode }> = ({
id,
children,
}) => {
const { setNodeRef, isOver } = useDroppable({ id });
const style: React.CSSProperties = {
background: isOver ? 'rgba(108, 99, 255, 0.1)' : 'transparent',
border: `2px dashed ${isOver ? '#6c63ff' : '#2a2d40'}`,
borderRadius: 8,
padding: 16,
minHeight: 100,
transition: 'all 0.2s',
};
return (
<div ref={setNodeRef} style={style}>
{children}
</div>
);
};
Sensors are configured at the DndContext level using the useSensors and useSensor hooks. The activationConstraint option on PointerSensor prevents accidental drags by requiring a minimum distance or delay before the drag activates. The KeyboardSensor uses a coordinateGetter function that determines how far the dragged element moves per keystroke.
import {
DndContext, PointerSensor, KeyboardSensor,
useSensor, useSensors,
} from '@dnd-kit/core';
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8, // 8px movement required before drag starts
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
The onDragEnd callback on DndContext receives the active (dragged item) and over (drop target) identifiers. The application is responsible for updating its own state in response—dnd-kit does not mutate data. For sortable lists, the arrayMove utility from @dnd-kit/sortable produces a new array with the item moved to its new position.
An internal component library uses dnd-kit’s sortable preset for reordering UI components in layout builders. Users drag component cards within a panel to rearrange their display order. The DragOverlay renders a translucent preview of the component card, while CSS transforms on the remaining items animate smoothly to fill the gap. The sorted order is persisted to the backend on onDragEnd, with optimistic updates for immediate visual feedback.
For task management interfaces, dnd-kit handles dragging items between multiple sortable containers (columns). The onDragOver event detects when an item crosses container boundaries, triggering a state update that moves the item from one column’s array to another. Custom collision detection using closestCorners ensures accurate targeting even when columns have different heights or contain items of varying sizes.
Internal dashboards leverage dnd-kit with rectSortingStrategy for grid-based widget reordering. Widgets snap to grid positions during drag, and the modifiers API constrains movement to the grid container boundaries. This prevents widgets from being dragged outside the dashboard area while maintaining smooth animation within the grid.
This pattern demonstrates a complete sortable list with smooth CSS transitions, drag overlay, and state management using arrayMove.
import { useState } from 'react';
import {
DndContext, closestCenter, DragEndEvent, DragStartEvent,
DragOverlay, PointerSensor, KeyboardSensor,
useSensor, useSensors,
} from '@dnd-kit/core';
import {
SortableContext, verticalListSortingStrategy,
useSortable, arrayMove, sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
interface Item {
id: string;
label: string;
}
const SortableItem: React.FC<{ item: Item }> = ({ item }) => {
const {
attributes, listeners, setNodeRef,
transform, transition, isDragging,
} = useSortable({ id: item.id });
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.4 : 1,
padding: '12px 16px',
background: '#1c1f2e',
border: '1px solid #2a2d40',
borderRadius: 8,
marginBottom: 8,
cursor: 'grab',
display: 'flex',
alignItems: 'center',
gap: 12,
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<span style={{ color: '#6b6785' }}>☰</span>
<span style={{ color: '#e8e6f0' }}>{item.label}</span>
</div>
);
};
const SortableList: React.FC = () => {
const [items, setItems] = useState<Item[]>([
{ id: '1', label: 'Configure data source' },
{ id: '2', label: 'Set up transformations' },
{ id: '3', label: 'Define output schema' },
{ id: '4', label: 'Validate pipeline' },
]);
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(null);
if (over && active.id !== over.id) {
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);
});
}
};
const activeItem = items.find((i) => i.id === activeId);
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map((item) => (
<SortableItem key={item.id} item={item} />
))}
</SortableContext>
<DragOverlay>
{activeItem ? <SortableItem item={activeItem} /> : null}
</DragOverlay>
</DndContext>
);
};
This pattern handles dragging items between separate sortable containers, using onDragOver to detect cross-container movement.
import { useState } from 'react';
import {
DndContext, DragOverEvent, DragEndEvent,
closestCorners, PointerSensor, useSensor, useSensors,
} from '@dnd-kit/core';
import {
SortableContext, verticalListSortingStrategy,
arrayMove,
} from '@dnd-kit/sortable';
type ColumnId = 'todo' | 'in-progress' | 'done';
interface Task {
id: string;
title: string;
}
interface Columns {
[key: string]: Task[];
}
const KanbanBoard: React.FC = () => {
const [columns, setColumns] = useState<Columns>({
'todo': [
{ id: 't1', title: 'Design schema' },
{ id: 't2', title: 'Write migrations' },
],
'in-progress': [
{ id: 't3', title: 'Implement API' },
],
'done': [
{ id: 't4', title: 'Set up CI' },
],
});
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
);
const findContainer = (id: string): string | undefined => {
// Check if the id is a column id
if (id in columns) return id;
// Otherwise find which column contains this task
return Object.keys(columns).find((key) =>
columns[key].some((task) => task.id === id)
);
};
const handleDragOver = (event: DragOverEvent) => {
const { active, over } = event;
if (!over) return;
const activeContainer = findContainer(active.id as string);
const overContainer = findContainer(over.id as string);
if (!activeContainer || !overContainer || activeContainer === overContainer) return;
setColumns((prev) => {
const activeItems = [...prev[activeContainer]];
const overItems = [...prev[overContainer]];
const activeIndex = activeItems.findIndex((t) => t.id === active.id);
const [movedItem] = activeItems.splice(activeIndex, 1);
overItems.push(movedItem);
return { ...prev, [activeContainer]: activeItems, [overContainer]: overItems };
});
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
const activeContainer = findContainer(active.id as string);
const overContainer = findContainer(over.id as string);
if (!activeContainer || !overContainer || activeContainer !== overContainer) return;
const items = columns[activeContainer];
const oldIndex = items.findIndex((t) => t.id === active.id);
const newIndex = items.findIndex((t) => t.id === over.id);
if (oldIndex !== newIndex) {
setColumns((prev) => ({
...prev,
[activeContainer]: arrayMove(prev[activeContainer], oldIndex, newIndex),
}));
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div style={{ display: 'flex', gap: 16 }}>
{Object.entries(columns).map(([columnId, tasks]) => (
<KanbanColumn key={columnId} id={columnId} tasks={tasks} />
))}
</div>
</DndContext>
);
};
SortableContext tracks items by their id values. Using array indices as IDs causes the library to lose track of items during reordering, producing visual glitches and incorrect drop positions. Always use stable, unique identifiers like database IDs or UUIDs.PointerSensor, every mousedown initiates a drag, preventing click events on interactive elements inside draggable items (buttons, links, inputs). Set activationConstraint: { distance: 8 } or { delay: 250, tolerance: 5 } to distinguish clicks from drags.DragOverlay, the dragged element moves in-place via CSS transforms, which can cause layout shifts, clipping by overflow containers, and z-index issues. The overlay renders in a portal above everything else and avoids these problems entirely.onDragEnd instead of creating a new array causes React to skip re-rendering, leaving the visual state inconsistent. Always use immutable updates with arrayMove or spread operators.announcements configuration on DndContext. Without them, screen reader users know an item is draggable but not where they are dropping it relative to other items.