Fix: dnd-kit Not Working — Drag Not Starting, Sort Order Not Updating, or Items Jumping on Drop
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:
DndContextalone provides no visual feedback — it only emits events. You must also applytransformstyles using theuseDraggabletransform value to move the element during drag. Without translating the element, the drag fires events but the item stays in place.- Sort order is not managed automatically —
useSortableandSortableContextcoordinate which items exist and their order, but updating the array is your responsibility inonDragEnd. IfsetItemsisn’t called correctly inhandleDragEnd, items snap back. - Sensors must be configured for touch — the default
PointerSensorworks for mouse and touch in most browsers, but touch events sometimes requireTouchSensorexplicitly. Additionally, iOS Safari blocks touch drag when the page can scroll unless you addpreventScrollOnStart. - 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. Scroll offset issues usually mean the portal positioning isn’t accounting for scroll correctly.
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 } 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',
};
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' },
]);
// Configure sensors — keyboard for accessibility, pointer for mouse/touch
const sensors = useSensors(
useSensor(PointerSensor),
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={items.map(i => i.id)} // Pass array of IDs
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.
For related drag and interaction issues, see Fix: React Event Handler Not Working and Fix: Framer Motion 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.