Drag and drop cheatsheet.

npm i @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Basic draggable + droppable

import { DndContext, useDraggable, useDroppable } from "@dnd-kit/core";

function Draggable({ id, children }) {
  const { attributes, listeners, setNodeRef, transform } = useDraggable({ id });
  const style = transform ? { transform: `translate(${transform.x}px, ${transform.y}px)` } : undefined;
  
  return (
    <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
      {children}
    </div>
  );
}

function Droppable({ id, children }) {
  const { isOver, setNodeRef } = useDroppable({ id });
  return (
    <div ref={setNodeRef} style={{ background: isOver ? "lightgreen" : "white" }}>
      {children}
    </div>
  );
}

function App() {
  return (
    <DndContext onDragEnd={(e) => console.log(e.over?.id, e.active.id)}>
      <Draggable id="card">Card</Draggable>
      <Droppable id="zone">Drop here</Droppable>
    </DndContext>
  );
}

Sortable list

import {
  arrayMove, SortableContext, useSortable, verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

function SortableItem({ id, children }) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
  const style = { transform: CSS.Transform.toString(transform), transition };
  return (
    <li ref={setNodeRef} style={style} {...attributes} {...listeners}>
      {children}
    </li>
  );
}

function List() {
  const [items, setItems] = useState(["a", "b", "c"]);
  
  function onDragEnd(e) {
    const { active, over } = e;
    if (!over || active.id === over.id) return;
    setItems((arr) => {
      const oldIdx = arr.indexOf(active.id);
      const newIdx = arr.indexOf(over.id);
      return arrayMove(arr, oldIdx, newIdx);
    });
  }
  
  return (
    <DndContext onDragEnd={onDragEnd}>
      <SortableContext items={items} strategy={verticalListSortingStrategy}>
        <ul>
          {items.map((i) => <SortableItem key={i} id={i}>{i}</SortableItem>)}
        </ul>
      </SortableContext>
    </DndContext>
  );
}

Sensors

import { useSensor, useSensors, PointerSensor, KeyboardSensor } from "@dnd-kit/core";

const sensors = useSensors(
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
  useSensor(KeyboardSensor),
);

<DndContext sensors={sensors} ...>

distance: 8 means drag only kicks in after 8px — fixes accidental drags on click.

DragOverlay (visual feedback)

import { DragOverlay } from "@dnd-kit/core";

const [active, setActive] = useState(null);

<DndContext
  onDragStart={(e) => setActive(e.active.id)}
  onDragEnd={() => setActive(null)}
>
  <List />
  <DragOverlay>
    {active && <Card>{active}</Card>}
  </DragOverlay>
</DndContext>

Between-list move (kanban)

function onDragEnd(e) {
  const { active, over } = e;
  if (!over) return;
  
  const fromCol = findColumn(active.id);
  const toCol = findColumn(over.id);
  
  if (fromCol === toCol) {
    // reorder within column
    setColumns({ ...columns, [fromCol]: arrayMove(...) });
  } else {
    // move between columns
    setColumns({
      ...columns,
      [fromCol]: columns[fromCol].filter(...),
      [toCol]: [...columns[toCol], active.id],
    });
  }
}

Accessibility

dnd-kit ships keyboard support (arrow keys + space). Provide screen-reader instructions:

<DndContext
  accessibility={{
    announcements: {
      onDragStart: ({ active }) => `Picked up ${active.id}`,
      onDragEnd: ({ active, over }) => over ? `Dropped on ${over.id}` : "Cancelled",
    },
  }}
>

HTML5 native DnD (simple cases)

<div
  draggable
  onDragStart={(e) => e.dataTransfer.setData("text/plain", id)}
/>

<div
  onDragOver={(e) => e.preventDefault()}
  onDrop={(e) => {
    const id = e.dataTransfer.getData("text/plain");
    handleDrop(id);
  }}
/>

Works but no touch support. Use dnd-kit for production.

File drop

function Dropzone() {
  function onDrop(e: React.DragEvent) {
    e.preventDefault();
    const files = Array.from(e.dataTransfer.files);
    uploadFiles(files);
  }
  
  return (
    <div onDragOver={(e) => e.preventDefault()} onDrop={onDrop}>
      Drop files here
    </div>
  );
}

react-dropzone

npm i react-dropzone
import { useDropzone } from "react-dropzone";

function Dropzone() {
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop: (accepted) => upload(accepted),
    accept: { "image/*": [] },
    maxSize: 10 * 1024 * 1024,
  });
  
  return (
    <div {...getRootProps()}>
      <input {...getInputProps()} />
      {isDragActive ? "Drop!" : "Drag here"}
    </div>
  );
}

Common mistakes

  • HTML5 DnD on mobile — broken. Use dnd-kit.
  • Tiny activation distance — accidental drags.
  • Forgetting preventDefault on dragOver — drop event never fires.
  • Mutating items array directly — stale React state.
  • DragOverlay not showing — forgot to wrap and track active state.

Read this next

If you want my kanban + sortable patterns, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .