@dnd-kit Drag and Drop Patterns
Quick Guide:
@dnd-kit/coresupplies the primitives —DndContext,useDraggable,useDroppable— and@dnd-kit/sortableaddsuseSortable,SortableContextandarrayMovefor reorderable lists. Nothing in the DOM is reordered during a drag: elements are moved by CSS transform and the state update happens on drop. Input arrives through sensors, which are separate plugins, so keyboard support is a sensor you add rather than a behaviour you get. Collision detection is pluggable and the choice depends on the layout.DragOverlayis needed whenever the dragged element would be clipped or unmounted mid-drag.
Detailed Resources:
- examples/core.md — draggable and droppable components, sortable lists, sensor setup, collision composition, announcements, drag handles
- examples/advanced.md — DragOverlay with sortables, multi-container Kanban, modifiers, custom collision detection, disabled items, item metadata
- reference.md — hook signatures and return values, event handler types, sorting strategies, collision algorithms, modifiers, default key bindings, applied ARIA attributes
Which path applies
- One list, items stay put. A single
SortableContext,closestCenter, and the transform on the item itself. Simplest, nothing extra to keep mounted. Follow examples/core.md. - Items cross containers, or the list scrolls. Both unmount or clip the dragged element mid-drag, so it needs a
DragOverlayand anactiveIdin state. Multi-container also wantsclosestCornersand anonDragOverhandler for the transfer. Follow examples/advanced.md. - Drop zones rather than reordering — a trash bin, an upload target, category bins.
@dnd-kit/corealone, no sortable package, and usuallypointerWithin. Follow examples/core.md Patterns 1 and 4.
Before writing @dnd-kit code
Wrap every participant in one <DndContext>. The hooks read sensors, collision state and the active drag from its context, and outside it they return inert values rather than throwing — so a missing provider looks like nothing happening.
Add a KeyboardSensor, with sortableKeyboardCoordinates where the list is sortable. Sensors are opt-in, so a context configured with pointer input alone cannot be operated from the keyboard at all. The coordinate getter is what makes arrow keys step between items instead of by fixed pixel offsets.
Give PointerSensor an activation constraint — a distance, or a delay with a tolerance for touch. Without one, every click begins a drag, and an item that is also a link or a button stops being clickable.
Keep DragOverlay mounted and render its children conditionally. The drop animation is played by the overlay as it unmounts its child; unmounting the overlay itself removes the thing that would animate.
Return a new array from the drop handler. arrayMove is pure and returns the reordered copy — it does not touch state, and mutating the existing array in place leaves React with nothing to re-render from.
Auto-detection: @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, @dnd-kit/modifiers, DndContext, useDraggable, useDroppable, useSortable, SortableContext, DragOverlay, useSensor, useSensors, PointerSensor, KeyboardSensor, TouchSensor, closestCenter, closestCorners, rectIntersection, pointerWithin, arrayMove, sortableKeyboardCoordinates, verticalListSortingStrategy, setActivatorNodeRef, CSS.Transform, restrictToVerticalAxis
Applies to:
- Sortable lists — reorderable todos, playlists, navigation, form field ordering
- Kanban boards and any layout where items move between containers
- Drop zones: trash bins, category bins, in-page upload targets
- Drag handles that restrict which part of an item starts a drag
- Keyboard-operable and screen-reader-announced drag interactions
- Constraining movement to an axis, a parent, or the viewport
Handled elsewhere:
- Reordering without a drag interaction — move-up/move-down controls are ordinary buttons over the same array operation
- Files dragged in from the operating system, which is the browser's own drag-and-drop and does not pass through a sensor
- Physics- or gesture-driven motion, where the interesting part is the animation rather than the drop target
- How dragged and hovered items look — every component here takes
styleandclassName, and the visual language is settled by whatever owns it - Persisting the new order — the drop handler produces an array, and where it is written is not this skill's concern
Nothing in the DOM moves during a drag. Items are displaced by CSS transform and the array is reordered once, on drop. That is why SortableContext's items must list the same ids in the same order as the rendered children — the strategy computes each item's displacement from its index in that array, and a mismatch computes the wrong offsets.
Input is plugins, not behaviour. A DndContext with no sensors responds to nothing. This is what makes keyboard support a deliberate addition rather than something that comes free, and it is the most common thing left out.
Accessibility is built in but not automatic. useDraggable applies role, aria-roledescription, tabindex and aria-describedby on its own; the announcements it makes default to the item's id, which tells a screen reader user nothing. Supplying announcements that describe position is the work.
Which packages
Reorderable lists -> @dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilities
Drop zones only -> @dnd-kit/core
Constrained movement -> add @dnd-kit/modifiers
Transform or DragOverlay
Items move between containers? -> DragOverlay (source unmounts mid-drag)
Draggable inside a scrolling or
virtualized container? -> DragOverlay (transform is clipped by overflow)
Preview should differ from the item? -> DragOverlay
None of these -> transform on the item; simpler, less state
Which collision algorithm
Single sortable list -> closestCenter forgiving, needs no overlap
Stacked containers (Kanban) -> closestCorners resolves nested targets
Precise zones (trash, bins) -> pointerWithin pointer must be inside
General drop targets -> rectIntersection the default
pointerWithin has no pointer to test during a keyboard drag, so it returns nothing — compose it with closestCenter as a fallback rather than using it alone.
Sorting strategies, modifiers and the full algorithm table are in reference.md.
Core patterns
Pattern 1: Basic drag and drop
DndContext connects the two hooks. useDraggable gives the element its listeners and ARIA attributes; useDroppable registers a target and reports when something is over it.
function Draggable({ id, children }: DraggableProps) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({ id });
return (
<div
ref={setNodeRef}
style={{ transform: CSS.Transform.toString(transform) }}
{...listeners}
{...attributes}
>
{children}
</div>
);
}
<DndContext
over }) => setDroppedIn(over ? String(over.id) : null)}
>
<Draggable id="item-1">Drag me</Draggable>
<Droppable id="zone-a">Drop zone</Droppable>
</DndContext>;
over is null when the drag ended outside every target, which is the cancel case.
Full code: examples/core.md
Pattern 2: Sortable lists
useSortable is useDraggable and useDroppable combined, so its id must be unique across both. arrayMove produces the reordered array on drop.
function handleDragEnd({ active, over }: DragEndEvent) {
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);
});
}
<DndContext collisionDetection={closestCenter}
<SortableContext
items={items.map((i) => i.id)} // same ids, same order as the children below
strategy={verticalListSortingStrategy}
>
{items.map((item) => (
<SortableItem key={item.id} id={item.id}>
{item.label}
</SortableItem>
))}
</SortableContext>
</DndContext>;
Full code: examples/core.md
Pattern 3: Sensors and activation constraints
Sensors decide what starts a drag. Compose them with useSensors.
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: ACTIVATION_DISTANCE_PX }, // click stays a click
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, // arrow keys step item to item
}),
);
<DndContext sensors={sensors}>{/* ... */}</DndContext>;
Separate MouseSensor and TouchSensor in place of PointerSensor where the two need different constraints — touch usually wants a delay and a tolerance, so that a scroll gesture is not read as a drag.
Full code: examples/core.md
Pattern 4: Collision detection
Pass one of the built-in algorithms, or compose them into a function when different targets need different behaviour.
const composedCollision: CollisionDetection = (args) => {
const pointerCollisions = pointerWithin(args);
if (pointerCollisions.length > 0) return pointerCollisions;
return closestCenter(args); // fallback: pointerWithin returns nothing for keyboard drags
};
<DndContext collisionDetection={composedCollision}>{/* ... */}</DndContext>;
Filtering args.droppableContainers before delegating is how one algorithm is applied to a trash zone and another to the list around it.
Full code: examples/core.md
Pattern 5: DragOverlay
The overlay renders the drag preview in its own layer, outside the list's overflow and independent of whether the source item still exists.
<DndContext
active }) => setActiveId(String(active.id))}
=> {
handleDragEnd(event);
setActiveId(null);
}}
=> setActiveId(null)} // Escape ends the drag too
>
{/* containers and sortable items */}
<DragOverlay>
{activeItem ? <ItemPreview item={activeItem} /> : null}
</DragOverlay>
</DndContext>
The child is presentational — calling useDraggable inside the overlay registers a second draggable for the item already being dragged.
Full code: examples/advanced.md
Pattern 6: Multi-container sortable (Kanban)
Each column is a droppable that also wraps its own SortableContext. onDragOver performs the transfer as the item crosses a boundary; onDragEnd settles the order within a column.
function handleDragOver({ active, over }: DragOverEvent) {
if (!over) return;
const from = findContainer(active.id);
const to = findContainer(over.id);
if (!from || !to || from === to) return; // same column: onDragEnd handles it
setColumns((prev) => moveBetweenColumns(prev, active.id, over.id, from, to));
}
<DndContext
collisionDetection={closestCorners}
>
{Object.entries(columns).map(([id, items]) => (
<KanbanColumn key={id} id={id} items={items} />
))}
<DragOverlay>
{activeItem ? <KanbanCard item={activeItem} /> : null}
</DragOverlay>
</DndContext>;
findContainer has to answer for both a column id and an item id, since over.id is whichever the collision resolved to.
Full code: examples/advanced.md
Pattern 7: Keyboard and screen reader announcements
The built-in announcements name the item by id. Replace them with position, which is what a screen reader user needs to track a move.
function createAnnouncements(items: string[]) {
const at = (id: UniqueIdentifier) =>
`position ${items.indexOf(String(id)) + 1} of ${items.length}`;
return {
onDragStart: ({ active }) => `Picked up item at ${at(active.id)}`,
onDragOver: ({ over }) =>
over ? `Moved to ${at(over.id)}` : "No longer over a drop target",
onDragEnd: ({ over }) =>
over ? `Dropped at ${at(over.id)}` : "Dropped outside a valid target",
onDragCancel: ({ active }) => `Cancelled. Returned to ${at(active.id)}`,
};
}
<DndContext announcements={createAnnouncements(itemIds)}>
{/* ... */}
</DndContext>;
screenReaderInstructions covers the other half — the instructions read when a draggable is focused, which default to English.
Full code: examples/core.md
Pattern 8: Drag handles
setActivatorNodeRef marks the element that starts the drag, separately from the element that moves.
<div ref={setNodeRef} style={style}>
<button
ref={setActivatorNodeRef}
{...listeners}
{...attributes}
aria-label={`Reorder ${label}`}
>
☰
</button>
{children}
</div>
The listeners and attributes go on the handle rather than the container, which is what leaves the rest of the item clickable and selectable.
Full code: examples/core.md
Pattern 9: Modifiers
Modifiers transform the drag position before it is applied. DndContext and DragOverlay take their own, independently.
<DndContext modifiers={[restrictToParentElement]}>
<DragOverlay modifiers={[restrictToWindowEdges]}>
{activeItem ? <ItemPreview item={activeItem} /> : null}
</DragOverlay>
</DndContext>
restrictToVerticalAxis on a vertical list removes the sideways drift that makes a reorder feel imprecise.
Full code: examples/advanced.md
Red flags
Breaks at runtime:
useDraggableon aDragOverlaychild — registers a second draggable for the item already being draggedSortableContext'sitemsnot matching the rendered children in id and order — displacement is computed from index in that array, so the animations and drop positions come out wronguseSortablesharing an id with another draggable or droppable — it registers as both, so its id must be unique across both sets, where a plainuseDraggableanduseDroppablemay share one id because they register in separate storespointerWithinused alone — a keyboard drag has no pointer, so it resolves no target and dropping does nothing
Surprising behaviour:
closestCenteron stacked containers resolves to the column rather than the card inside it;closestCornersmeasures all four corners and resolves the nested targetrectSortingStrategyis the default and does not support virtualization — the vertical and horizontal list strategies doCSS.Transform.toString()includesscaleX/scaleY;CSS.Translate.toString()is the position-only form, which is usually what a list wantsCSS.Transform.toString(null)returnsundefined, which is safe to assign tostyle.transformDragOverlayis not portalled by default, so it is still subject to an ancestor'soverflowand stacking context — wrap it increatePortalto escape themactive.data.currentandover.data.currentcarry whatever was passed asdata, which is how one handler tells a card from a column without looking either up- Screen reader instructions and announcements default to English, and are replaced rather than translated
- Default keys are Space or Enter to pick up and drop, arrows to move, Escape to cancel —
onDragCancelis the Escape path and has to reset the same stateonDragEnddoes