Fix: dnd-kit Not Working — Drag Not Starting, Sort Order Not Updating, or Items Jumping on Drop
Part of: React & Frontend Errors
Quick Answer
How to fix dnd-kit issues — DndContext setup, sensors configuration, useSortable with SortableContext, drag overlays, collision detection algorithms, and accessible drag and drop.
The Problem
Dragging does nothing — the item doesn’t move:
import { DndContext } from '@dnd-kit/core';
import { useDraggable } from '@dnd-kit/core';
function DraggableItem() {
const { attributes, listeners, setNodeRef } = useDraggable({ id: 'item-1' });
return (
<div ref={setNodeRef} {...listeners} {...attributes}>
Drag me
</div>
);
}
// Clicking and dragging — nothing happensOr items reorder during the drag but snap back to the original order on drop:
const [items, setItems] = useState(['A', 'B', 'C']);
function handleDragEnd(event) {
const { active, over } = event;
// Reordering logic runs but items reset to original order
}Or the drag overlay renders in the wrong position or doesn’t follow the cursor:
Items shift as if the overlay is offset by the scroll positionOr touch drag doesn’t work on mobile:
Mouse drag works fine in desktop Chrome but touch drag on iOS does nothingWhy This Happens
dnd-kit is a low-level, modular drag-and-drop library for React. It intentionally ships with almost no default behavior. The library gives you the event plumbing — start, move, over, end — but every visual concern (moving the DOM element, updating the array, rendering a drag preview) is opt-in. Most “not working” reports come from skipping one of these opt-ins.
A second source of confusion is the split between DndContext and SortableContext. DndContext only knows about draggables and droppables. It does not understand order. SortableContext adds the concept of an ordered list on top, but it expects the items array to be referentially stable between renders — passing items.map(i => i.id) inline creates a new array every render and can break the internal index cache used by verticalListSortingStrategy. Memoize the ID array if your list is large.
A third trap is overlay placement. DragOverlay renders via a portal at the document root. If any ancestor of DndContext uses transform, filter, perspective, or will-change, the browser creates a new containing block for fixed-positioned descendants, which throws off the overlay coordinates. The cure is to keep DragOverlay as a sibling of the list, not nested inside a transformed wrapper.
DndContextalone provides no visual feedback — it only emits events. You must applytransformstyles using theuseDraggabletransform value to move the element during drag.- Sort order is not managed automatically —
useSortableandSortableContextcoordinate which items exist and their order, but updating the array is your responsibility inonDragEnd. - Sensors must be configured for touch — the default
PointerSensorworks for mouse and touch in most browsers, but iOS Safari blocks touch drag when the page can scroll unless you addtouchAction: 'none'. - Drag overlays require a
DragOverlaycomponent — without it, dnd-kit moves the actual DOM node. With aDragOverlay, the original item stays in place and a floating clone follows the cursor.
Diagnostic Timeline
A senior dev’s first guess is usually “you forgot to set up sensors.” That is rarely the real cause. Here is the order to actually check:
Minute 0 — Reproduce in isolation. Strip the component down to a 3-item list. If the bare list works, the bug is in items array stability, custom collision detection, or overlay placement — not in dnd-kit itself.
Minute 2 — Check SortableContext membership. Open React DevTools and confirm the SortableContext is rendering with an items prop that contains the same IDs as the children’s useSortable({ id }) calls. The single most common cause of “drag does nothing” is that the items array passed to SortableContext contains objects, not the ID strings the children registered with.
Minute 5 — Check items array stability. If items is recomputed every render (e.g., data?.users?.map(u => u.id) inline), the SortableContext loses index state. Wrap in useMemo keyed on the underlying data.
Minute 8 — Check transform application. Inspect the dragged item in DevTools mid-drag. If style shows transform: translate3d(0px, 0px, 0), your style object isn’t being spread onto the same element as setNodeRef. This is the “drag fires but item doesn’t move” case.
Minute 12 — Check overlay portal placement. Look at the rendered DOM during drag. If DragOverlay content appears as a sibling of <body> but visually offset, walk up the ancestor chain of your DndContext looking for any element with transform, filter, or will-change set. Move DndContext outside that wrapper.
Minute 15 — Check touch. On mobile, add touchAction: 'none' to the draggable’s style. Without it, the browser captures the touch for scrolling before dnd-kit can claim it. If you also need long-press-to-drag, switch to TouchSensor with activationConstraint: { delay: 250, tolerance: 5 }.
Fix 1: Set Up Basic Draggable Items
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilitiesimport {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useState, useMemo } from 'react';
// Sortable item component
function SortableItem({ id, label }: { id: string; label: string }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
// REQUIRED — apply transform to move the item
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
touchAction: 'none' as const, // Required for touch drag on iOS
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{label}
</div>
);
}
// Parent component with sortable list
function SortableList() {
const [items, setItems] = useState([
{ id: 'a', label: 'Item A' },
{ id: 'b', label: 'Item B' },
{ id: 'c', label: 'Item C' },
{ id: 'd', label: 'Item D' },
]);
// Memoize the ID array — referential stability matters for SortableContext
const itemIds = useMemo(() => items.map(i => i.id), [items]);
// Configure sensors — keyboard for accessibility, pointer for mouse/touch
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 }, // Prevent accidental drag on click
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) return; // No movement
setItems(items => {
const oldIndex = items.findIndex(i => i.id === active.id);
const newIndex = items.findIndex(i => i.id === over.id);
// arrayMove is the key — it reorders the array correctly
return arrayMove(items, oldIndex, newIndex);
});
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={itemIds}
strategy={verticalListSortingStrategy}
>
{items.map(item => (
<SortableItem key={item.id} id={item.id} label={item.label} />
))}
</SortableContext>
</DndContext>
);
}Fix 2: Add a Drag Overlay for Better UX
The drag overlay renders a floating clone that follows the cursor while the original item stays in place:
import {
DndContext,
DragOverlay,
closestCenter,
useSensor,
useSensors,
PointerSensor,
KeyboardSensor,
DragStartEvent,
DragEndEvent,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
arrayMove,
} from '@dnd-kit/sortable';
import { useState } from 'react';
function SortableListWithOverlay() {
const [items, setItems] = useState(['A', 'B', 'C', 'D']);
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(PointerSensor));
function handleDragStart(event: DragStartEvent) {
setActiveId(String(event.active.id));
}
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
setActiveId(null);
if (!over || active.id === over.id) return;
setItems(items => {
const oldIndex = items.indexOf(String(active.id));
const newIndex = items.indexOf(String(over.id));
return arrayMove(items, oldIndex, newIndex);
});
}
function handleDragCancel() {
setActiveId(null); // Reset when drag is cancelled (Escape key)
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map(id => (
<SortableItem key={id} id={id} label={`Item ${id}`} />
))}
</SortableContext>
{/* Overlay renders outside the list — uses a portal to avoid z-index issues */}
<DragOverlay>
{activeId ? (
<div style={{ cursor: 'grabbing', boxShadow: '0 5px 15px rgba(0,0,0,0.2)' }}>
Item {activeId}
</div>
) : null}
</DragOverlay>
</DndContext>
);
}Fix 3: Fix Touch and Mobile Drag
Enable touch drag on iOS and Android:
import {
PointerSensor,
TouchSensor,
MouseSensor,
KeyboardSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
// Option 1 — PointerSensor with constraints (works for most cases)
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
// Require 8px of movement before starting drag
// Prevents accidental drags on tap
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Option 2 — Separate Mouse and Touch sensors
// Useful when you need different constraints per input type
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 5,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250, // Hold 250ms before drag starts
tolerance: 5, // Allow 5px movement during delay
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Option 3 — Prevent default scroll while dragging (iOS fix)
// Add to the draggable element's style
const { listeners, setNodeRef, transform, transition } = useSortable({ id });
// Apply touch-action: none to disable browser scroll during drag
const style = {
transform: CSS.Transform.toString(transform),
transition,
touchAction: 'none', // Critical for iOS
};Fix 4: Multiple Containers (Kanban-Style)
Moving items between different lists requires onDragOver in addition to onDragEnd:
import {
DndContext,
DragOverlay,
closestCorners,
useSensor,
useSensors,
PointerSensor,
DragStartEvent,
DragOverEvent,
DragEndEvent,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
arrayMove,
} from '@dnd-kit/sortable';
import { useState } from 'react';
type Item = { id: string; content: string };
type Containers = { [key: string]: Item[] };
function KanbanBoard() {
const [containers, setContainers] = useState<Containers>({
todo: [{ id: '1', content: 'Task 1' }, { id: '2', content: 'Task 2' }],
doing: [{ id: '3', content: 'Task 3' }],
done: [{ id: '4', content: 'Task 4' }],
});
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(PointerSensor));
// Find which container an item belongs to
function findContainer(id: string) {
return Object.keys(containers).find(key =>
containers[key].some(item => item.id === id)
);
}
function handleDragStart(event: DragStartEvent) {
setActiveId(String(event.active.id));
}
function handleDragOver(event: DragOverEvent) {
const { active, over } = event;
if (!over) return;
const activeContainer = findContainer(String(active.id));
const overContainer = findContainer(String(over.id)) || String(over.id);
if (!activeContainer || !overContainer || activeContainer === overContainer) return;
// Move item to new container on hover
setContainers(prev => {
const activeItems = prev[activeContainer];
const overItems = prev[overContainer];
const activeIndex = activeItems.findIndex(i => i.id === active.id);
const overIndex = overItems.findIndex(i => i.id === over.id);
const movedItem = activeItems[activeIndex];
return {
...prev,
[activeContainer]: activeItems.filter(i => i.id !== active.id),
[overContainer]: [
...overItems.slice(0, overIndex + 1),
movedItem,
...overItems.slice(overIndex + 1),
],
};
});
}
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
setActiveId(null);
if (!over) return;
const activeContainer = findContainer(String(active.id));
const overContainer = findContainer(String(over.id)) || String(over.id);
if (!activeContainer || activeContainer !== overContainer) return;
// Reorder within the same container
setContainers(prev => {
const items = prev[activeContainer];
const oldIndex = items.findIndex(i => i.id === active.id);
const newIndex = items.findIndex(i => i.id === over.id);
return {
...prev,
[activeContainer]: arrayMove(items, oldIndex, newIndex),
};
});
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div style={{ display: 'flex', gap: '16px' }}>
{Object.entries(containers).map(([id, items]) => (
<div key={id} style={{ flex: 1 }}>
<h3>{id}</h3>
<SortableContext
id={id}
items={items.map(i => i.id)}
strategy={verticalListSortingStrategy}
>
{items.map(item => (
<SortableItem key={item.id} id={item.id} label={item.content} />
))}
</SortableContext>
</div>
))}
</div>
<DragOverlay>
{activeId ? <div>{activeId}</div> : null}
</DragOverlay>
</DndContext>
);
}Fix 5: Collision Detection Algorithms
Choosing the wrong collision detection algorithm causes items to snap to unexpected targets:
import {
closestCenter,
closestCorners,
rectIntersection,
pointerWithin,
getFirstCollision,
} from '@dnd-kit/core';
// closestCenter — best for sortable lists
// Finds the droppable whose center point is closest to the drag point
// Use when dragging items within a single list
<DndContext collisionDetection={closestCenter}>
// closestCorners — best for grids and multi-container layouts
// Compares all 4 corners of the dragged item to find the closest target
<DndContext collisionDetection={closestCorners}>
// rectIntersection — overlap-based detection
// Triggers when dragged item overlaps at least partially with a droppable
<DndContext collisionDetection={rectIntersection}>
// pointerWithin — pointer position detection
// Triggers when the pointer is directly over a droppable
// Good for large droppable areas
<DndContext collisionDetection={pointerWithin}>
// Custom hybrid for Kanban — try pointer first, fall back to closest corners
function customCollisionDetection(args) {
// Use pointer detection for container-level drops
const pointerCollisions = pointerWithin(args);
if (pointerCollisions.length > 0) {
return pointerCollisions;
}
// Fall back to rectangle intersection for item-level drops
return getFirstCollision(rectIntersection(args));
}Fix 6: Accessible Drag and Drop
dnd-kit has built-in keyboard support, but you need to configure announcements:
import { DndContext, Announcements } from '@dnd-kit/core';
// Custom screen reader announcements
const announcements: Announcements = {
onDragStart({ active }) {
return `Picked up ${active.data.current?.label}. Use arrow keys to move.`;
},
onDragOver({ active, over }) {
if (over) {
return `${active.data.current?.label} is over ${over.data.current?.label}.`;
}
return `${active.data.current?.label} is over an empty area.`;
},
onDragEnd({ active, over }) {
if (over) {
return `${active.data.current?.label} was dropped over ${over.data.current?.label}.`;
}
return `${active.data.current?.label} was dropped.`;
},
onDragCancel({ active }) {
return `Drag cancelled. ${active.data.current?.label} returned to original position.`;
},
};
// Pass custom data for announcements
const { attributes, listeners, setNodeRef } = useSortable({
id,
data: { label: 'Task 1' }, // Used in announcements above
});
<DndContext accessibility={{ announcements }}>
{/* ... */}
</DndContext>Still Not Working?
Items snap back to original position on drop — arrayMove is the key. Make sure you’re calling it in handleDragEnd with the correct old and new indices. A common mistake is using indexOf on the wrong array or before the state update. Use the functional form of setItems to guarantee you’re working with the latest state.
Drag starts but items don’t visually move — you’re missing the transform style. Every draggable item must apply CSS.Transform.toString(transform) to its inline styles. Without this, dnd-kit tracks the drag position but has no way to move the DOM element. Check that the style object is being applied to the same element as setNodeRef.
Scroll position causes drag overlay to appear in the wrong place — DragOverlay renders in a portal at the document root. If your page is scrolled, coordinates can be offset. Make sure the DragOverlay is a direct child of DndContext, not nested inside a scrollable container with position: relative. Also verify that no parent element has transform: translateZ(0) applied, which creates a new stacking context and breaks fixed positioning.
over is always null in onDragEnd — the draggable and droppable need to overlap for a collision to register. If the drag ends before overlapping a droppable, over is null. Also check that droppable elements have non-zero dimensions — if a container is empty and has height: 0, nothing can be dropped into it. Add a minimum height to empty containers.
activatorEvent triggers on click inside the dragged item — by default, PointerSensor activates on any pointer-down. If your items contain buttons or links, clicking them triggers a drag. Add activationConstraint: { distance: 8 } so the user must move 8 pixels before the drag begins. For finer control, attach listeners only to a drag handle inside the item, not the whole element.
Items reorder visually but the new order is lost on refresh — dnd-kit does not persist anything. You must save the reordered array yourself in onDragEnd, either to local state plus a backend mutation or to localStorage. If you persist by index, remember that React’s render order must match the persisted order — re-fetching from the server in a different order will undo the drag.
Drag works in development but is broken in production with React 18 strict mode — strict mode double-invokes useDraggable initialization. If your id is computed from a non-stable source (like Math.random() or an array index that shifts), the second invocation registers a different droppable. Use stable IDs from your data model, not generated per render.
For related drag and interaction issues, see Fix: Framer Motion Not Working, Fix: React Each Child Should Have Unique Key, Fix: React Portal Event Bubbling, and Fix: CSS z-index Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.