Drag and drop cheatsheet.
dnd-kit (recommended)
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
preventDefaulton 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 .