dnd-kit Reference

dnd-kit is a lightweight, modular drag and drop toolkit for React. It provides flexible primitives for building drag-and-drop interfaces.

Import

// Core
import { GripVertical, Plus } from "lucide-react";

interface Task {
  id: string;
  title: string;
}

function SortableTask({ task }: { task: Task }) {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id });

  const style: React.CSSProperties = {
    transform: CSS.Translate.toString(transform),
    transition: isDragging ? undefined : transition,
    zIndex: isDragging ? 10 : undefined,
    position: "relative",
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={`p-3 flex items-center gap-2 ${isDragging ? "opacity-80 shadow-lg" : ""}`}
    >
      <button {...attributes} {...listeners} className="cursor-grab active:cursor-grabbing touch-none">
        <GripVertical className="w-4 h-4 text-muted-foreground" />
      </button>
      <span className="flex-1">{task.title}</span>
    </div>
  );
}

export default function App() {
  const [tasks, setTasks] = usePersistentItem<Task[]>("tasks", []);
  const [newTask, setNewTask] = React.useState("");

  const addTask = () => {
    if (!newTask.trim()) return;
    setTasks(prev => [...prev, { id: crypto.randomUUID(), title: newTask.trim() }]);
    setNewTask("");
  };

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    if (over && active.id !== over.id) {
      setTasks(prev => {
        const oldIndex = prev.findIndex(t => t.id === active.id);
        const newIndex = prev.findIndex(t => t.id === over.id);
        return arrayMove(prev, oldIndex, newIndex);
      });
    }
  };

  return (
    <div>
      <div>
        <div>
          <h3>Tasks</h3>
        </div>
        <div className="space-y-4">
          <div className="flex gap-2">
            <input
              value={newTask}
              onChange={e => setNewTask(e.target.value)}
              placeholder="New task..."
              onKeyDown={e => e.key === "Enter" && addTask()}
            />
            <button onClick={addTask} size="icon">
              <Plus className="w-4 h-4" />
            </button>
          </div>

          <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
            <SortableContext items={tasks} strategy={verticalListSortingStrategy}>
              <div className="space-y-2">
                {tasks.map(task => (
                  <SortableTask key={task.id} task={task} />
                ))}
              </div>
            </SortableContext>
          </DndContext>
        </div>
      </div>
    </div>
  );
}

Smooth Drag Animation

For the smoothest drag experience, optimize the style handling:

function SortableItem({ item }: { item: Item }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: item.id });

  // Optimized style for smooth dragging
  const style: React.CSSProperties = {
    // Use Translate instead of Transform - translate-only is GPU-accelerated
    transform: CSS.Translate.toString(transform),
    // Disable transition while dragging to prevent lag
    transition: isDragging ? undefined : transition,
    // Ensure dragged item renders above others
    zIndex: isDragging ? 10 : undefined,
    position: "relative",
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={`p-4 flex items-center gap-3 ${
        isDragging
          ? "opacity-80 shadow-xl scale-[1.02]"
          : "transition-colors duration-150 hover:bg-muted/50"
      }`}
    >
      <button
        {...attributes}
        {...listeners}
        className="cursor-grab active:cursor-grabbing touch-none"
      >
        <GripVertical className="w-5 h-5 text-muted-foreground" />
      </button>
      <span>{item.text}</span>
    </div>
  );
}

Key optimizations:

  1. CSS.Translate.toString() - Use instead of CSS.Transform.toString() for GPU-accelerated translate-only transforms
  2. Disable transition while dragging - transition: isDragging ? undefined : transition prevents the drag animation from lagging
  3. Explicit zIndex - Ensures the dragged item renders above siblings
  4. Avoid transition-all - Use specific transitions like transition-colors only when not dragging to avoid conflicts
  5. Visual feedback - Add scale and shadow effects to make the dragged item feel lifted

Best Practices

  1. Use touch-none on drag handles for proper touch behavior
  2. Use cursor-grab and active:cursor-grabbing on drag handles for visual feedback
  3. Use CSS.Translate.toString() for smoother, GPU-accelerated transforms
  4. Disable transitions while dragging to prevent animation lag
  5. Use closestCenter for vertical/horizontal lists
  6. Keep IDs stable - use unique, persistent IDs for items
  7. Use arrayMove helper for reordering arrays