Skip to content

Fix: dnd-kit Not Working — Drag Not Starting, Sort Order Not Updating, or Items Jumping on Drop

FixDevs ·

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 happens

Or 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 position

Or touch drag doesn’t work on mobile:

Mouse drag works fine in desktop Chrome but touch drag on iOS does nothing

Why This Happens

dnd-kit is a low-level, modular drag-and-drop library for React. It intentionally ships with almost no default behavior:

  • DndContext alone provides no visual feedback — it only emits events. You must also apply transform styles using the useDraggable transform 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 automaticallyuseSortable and SortableContext coordinate which items exist and their order, but updating the array is your responsibility in onDragEnd. If setItems isn’t called correctly in handleDragEnd, items snap back.
  • Sensors must be configured for touch — the default PointerSensor works for mouse and touch in most browsers, but touch events sometimes require TouchSensor explicitly. Additionally, iOS Safari blocks touch drag when the page can scroll unless you add preventScrollOnStart.
  • Drag overlays require a DragOverlay component — without it, dnd-kit moves the actual DOM node. With a DragOverlay, 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/utilities
import {
  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 droparrayMove 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 placeDragOverlay 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles