Skip to main content
Back to Blog
Tutorials
3 min read
November 9, 2024

How to Build a Drag-and-Drop Interface with dnd-kit in React

Build drag-and-drop interfaces in React with dnd-kit. Sortable lists, kanban boards, and accessible drag interactions.

Ryel Banfield

Founder & Lead Developer

dnd-kit is the modern, accessible drag-and-drop toolkit for React. It supports sortable lists, kanban boards, and complex multi-container drag interactions.

Step 1: Install dnd-kit

pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Step 2: Basic Sortable List

"use client";

import { useState } from "react";
import {
  DndContext,
  closestCenter,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  type DragEndEvent,
} from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  verticalListSortingStrategy,
  useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

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

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

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={`flex items-center gap-3 rounded-lg border bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 ${
        isDragging ? "z-50 opacity-50 shadow-lg" : ""
      }`}
    >
      <button
        {...attributes}
        {...listeners}
        className="cursor-grab text-gray-400 active:cursor-grabbing"
        aria-label={`Drag ${item.title}`}
      >
        ⠿
      </button>
      <span className="text-sm font-medium">{item.title}</span>
    </div>
  );
}

export function SortableList() {
  const [items, setItems] = useState<Item[]>([
    { id: "1", title: "Design homepage" },
    { id: "2", title: "Build contact form" },
    { id: "3", title: "Add SEO meta tags" },
    { id: "4", title: "Deploy to production" },
  ]);

  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  );

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;
    if (!over || active.id === over.id) return;

    setItems((prev) => {
      const oldIndex = prev.findIndex((i) => i.id === active.id);
      const newIndex = prev.findIndex((i) => i.id === over.id);
      return arrayMove(prev, oldIndex, newIndex);
    });
  }

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

Step 3: Drag Overlay

Add a visual overlay that follows the cursor:

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

const [activeId, setActiveId] = useState<string | null>(null);
const activeItem = items.find((i) => i.id === activeId);

<DndContext
  onDragStart={(event) => setActiveId(String(event.active.id))}
  onDragEnd={(event) => {
    setActiveId(null);
    handleDragEnd(event);
  }}
  onDragCancel={() => setActiveId(null)}
>
  <SortableContext items={items}>
    {items.map((item) => (
      <SortableItem key={item.id} item={item} />
    ))}
  </SortableContext>

  <DragOverlay>
    {activeItem ? (
      <div className="rounded-lg border bg-white p-4 shadow-xl">
        {activeItem.title}
      </div>
    ) : null}
  </DragOverlay>
</DndContext>

Step 4: Kanban Board

"use client";

import { useState } from "react";
import {
  DndContext,
  DragOverlay,
  closestCorners,
  type DragEndEvent,
  type DragOverEvent,
  type DragStartEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";

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

const COLUMNS = ["To Do", "In Progress", "Done"];

export function KanbanBoard() {
  const [tasks, setTasks] = useState<Task[]>([
    { id: "1", title: "Research competitors", column: "To Do" },
    { id: "2", title: "Design wireframes", column: "To Do" },
    { id: "3", title: "Build prototype", column: "In Progress" },
    { id: "4", title: "Write copy", column: "Done" },
  ]);
  const [activeTask, setActiveTask] = useState<Task | null>(null);

  function handleDragStart(event: DragStartEvent) {
    const task = tasks.find((t) => t.id === event.active.id);
    setActiveTask(task || null);
  }

  function handleDragOver(event: DragOverEvent) {
    const { active, over } = event;
    if (!over) return;

    const activeTask = tasks.find((t) => t.id === active.id);
    const overColumn = COLUMNS.includes(String(over.id))
      ? String(over.id)
      : tasks.find((t) => t.id === over.id)?.column;

    if (activeTask && overColumn && activeTask.column !== overColumn) {
      setTasks((prev) =>
        prev.map((t) =>
          t.id === active.id ? { ...t, column: overColumn } : t
        )
      );
    }
  }

  function handleDragEnd(event: DragEndEvent) {
    setActiveTask(null);
    // Persist order to database here
  }

  return (
    <DndContext
      collisionDetection={closestCorners}
      onDragStart={handleDragStart}
      onDragOver={handleDragOver}
      onDragEnd={handleDragEnd}
    >
      <div className="grid grid-cols-3 gap-4">
        {COLUMNS.map((column) => {
          const columnTasks = tasks.filter((t) => t.column === column);
          return (
            <div
              key={column}
              className="rounded-xl bg-gray-100 p-4 dark:bg-gray-800"
            >
              <h3 className="mb-3 text-sm font-semibold">{column}</h3>
              <SortableContext
                items={columnTasks}
                strategy={verticalListSortingStrategy}
              >
                <div className="space-y-2">
                  {columnTasks.map((task) => (
                    <SortableItem key={task.id} item={task} />
                  ))}
                </div>
              </SortableContext>
            </div>
          );
        })}
      </div>

      <DragOverlay>
        {activeTask ? (
          <div className="rounded-lg border bg-white p-3 shadow-xl">
            {activeTask.title}
          </div>
        ) : null}
      </DragOverlay>
    </DndContext>
  );
}

Step 5: Grid Layout Sorting

import { rectSortingStrategy } from "@dnd-kit/sortable";

<SortableContext items={items} strategy={rectSortingStrategy}>
  <div className="grid grid-cols-3 gap-4">
    {items.map((item) => (
      <SortableGridItem key={item.id} item={item} />
    ))}
  </div>
</SortableContext>

Step 6: Restrict Drag Axis

import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";

<DndContext modifiers={[restrictToVerticalAxis, restrictToParentElement]}>
  {/* content */}
</DndContext>

Step 7: Persist Order

async function handleDragEnd(event: DragEndEvent) {
  const { active, over } = event;
  if (!over || active.id === over.id) return;

  const updated = arrayMove(/* ... */);
  setItems(updated);

  // Persist to database
  await fetch("/api/reorder", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      items: updated.map((item, index) => ({
        id: item.id,
        order: index,
      })),
    }),
  });
}

Need Interactive UI Features?

We build web applications with drag-and-drop, kanban boards, and other advanced interactive features. Contact us for a consultation.

drag and dropdnd-kitReactsortabletutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles