React Drag and Drop: Sortable List and Kanban Board

For most React apps, dnd-kit is the simplest solid choice: an MIT licensed toolkit with pointer, touch and keyboard sensors built in. Install @dnd-kit/react, wrap the area in DragDropProvider, give each card useSortable with an id, index and group, then update your state in onDragEnd. The native HTML drag events work too, with more hand-written code.

Two ways to add drag and drop
dnd-kit (@dnd-kit/react)Native HTML drag and drop
Installnpm install @dnd-kit/reactNothing, built into browsers
KeyboardKeyboard sensor includedYou build your own key controls
TouchTouch and pointer sensors includedVaries by browser; test on phones
Sortable listsuseSortable from @dnd-kit/react/sortableYour own index maths in drop handlers
Best forKanban boards, sortable lists, gridsDropping files or simple one-off drops

Updated

Below is a working board you can drag cards around on right now, with arrow buttons for anyone not using a mouse. Under it you will find the dnd-kit code for the same sortable list and multi-column board, plus the plain browser events version for comparison.

Try it: a working kanban board

Drag cards between To do, Doing and Done, or use the move buttons on each card.

Sortable list with dnd-kit

Install with npm install @dnd-kit/react. Each item knows its own index; after a drop, source.initialIndex and source.index tell you where it moved.

SortableList.jsx

Reorders an array of task names. The copy-then-splice keeps the old state untouched.

import { useState } from "react";
import { DragDropProvider } from "@dnd-kit/react";
import { useSortable, isSortable } from "@dnd-kit/react/sortable";

function Task({ id, index }) {
  const { ref } = useSortable({ id, index });
  return <li ref={ref} className="rounded-lg border bg-white p-3">{id}</li>;
}

export default function SortableList() {
  const [tasks, setTasks] = useState(["Write brief", "Pick photos", "Draft homepage", "Review copy"]);

  function handleDragEnd(event) {
    if (event.canceled) return;
    const { source } = event.operation;
    if (!isSortable(source) || source.initialIndex === source.index) return;
    setTasks((list) => {
      const next = [...list];
      const [moved] = next.splice(source.initialIndex, 1);
      next.splice(source.index, 0, moved);
      return next;
    });
  }

  return (
    <DragDropProvider onDragEnd={handleDragEnd}>
      <ul className="space-y-2">
        {tasks.map((id, index) => <Task key={id} id={id} index={index} />)}
      </ul>
    </DragDropProvider>
  );
}

Kanban board with dnd-kit

Add a group to useSortable so cards can travel between columns. source.initialGroup and source.group are the old and new column names.

KanbanBoard.jsx

Three columns held in one object. Moving inside a column reorders it; moving across removes the card from one array and inserts it into the other.

import { useState } from "react";
import { DragDropProvider } from "@dnd-kit/react";
import { useSortable, isSortable } from "@dnd-kit/react/sortable";

function Card({ id, index, column }) {
  const { ref } = useSortable({ id, index, group: column, type: "card", accept: "card" });
  return <li ref={ref} className="rounded-lg border bg-white p-3">{id}</li>;
}

export default function KanbanBoard() {
  const [board, setBoard] = useState({
    todo: ["Logo", "Pricing table"],
    doing: ["Hero section"],
    done: [],
  });

  function handleDragEnd(event) {
    if (event.canceled) return;
    const { source } = event.operation;
    if (!isSortable(source)) return;
    const { initialIndex, index, initialGroup, group } = source;
    setBoard((b) => {
      const from = [...b[initialGroup]];
      const [card] = from.splice(initialIndex, 1);
      if (initialGroup === group) {
        from.splice(index, 0, card);
        return { ...b, [group]: from };
      }
      const to = [...b[group]];
      to.splice(index, 0, card);
      return { ...b, [initialGroup]: from, [group]: to };
    });
  }

  return (
    <DragDropProvider onDragEnd={handleDragEnd}>
      <div className="grid grid-cols-3 gap-4">
        {Object.entries(board).map(([column, cards]) => (
          <section key={column}>
            <h2 className="mb-2 font-semibold capitalize">{column}</h2>
            <ul className="min-h-24 space-y-2 rounded-xl bg-slate-100 p-2">
              {cards.map((id, index) => <Card key={id} id={id} index={index} column={column} />)}
            </ul>
          </section>
        ))}
      </div>
    </DragDropProvider>
  );
}

The same idea with native HTML drag events

No package needed. Note the preventDefault in onDragOver: without it, the drop never fires.

NativeColumns.jsx

Moves a card to the end of the column it is dropped on. Reordering inside a column would need extra index logic.

import { useState } from "react";

export default function NativeColumns() {
  const [board, setBoard] = useState({ todo: ["Logo", "Menu"], done: [] });

  function drop(e, column) {
    e.preventDefault();
    const { card, from } = JSON.parse(e.dataTransfer.getData("text/plain"));
    if (from === column) return;
    setBoard((b) => ({
      ...b,
      [from]: b[from].filter((c) => c !== card),
      [column]: [...b[column], card],
    }));
  }

  return (
    <div style={{ display: "flex", gap: 16 }}>
      {Object.entries(board).map(([column, cards]) => (
        <ul key={column} onDragOver={(e) => e.preventDefault()} onDrop={(e) => drop(e, column)}
            style={{ minHeight: 120, minWidth: 160, padding: 8, background: "#f1f5f9" }}>
          {cards.map((card) => (
            <li key={card} draggable
                onDragStart={(e) => e.dataTransfer.setData("text/plain", JSON.stringify({ card, from: column }))}>
              {card}
            </li>
          ))}
        </ul>
      ))}
    </div>
  );
}

HTML drag events reference

The seven drag events and where they fire
EventFires onWhen
dragstartThe dragged elementThe drag begins; set dataTransfer here
dragThe dragged elementRepeatedly while dragging
dragenterA possible targetThe dragged item enters it
dragoverA possible targetRepeatedly while over it; call preventDefault to allow a drop
dragleaveA possible targetThe dragged item leaves it
dropThe targetThe item is released on a valid target; read dataTransfer here
dragendThe dragged elementThe drag finishes, dropped or cancelled

State first, dragging second

A board is an object of arrays: one array of card ids per column. Dragging only decides which card moved, where it came from and where it landed. Every library hands you those four facts, and your code turns them into a new object.

Write that move function as a pure helper and test it without a browser. dnd-kit even ships one, move from @dnd-kit/helpers, which takes your items and the drag event and returns the updated copy.

Why the native API is harder than it looks

The browser fires seven events (dragstart, drag, dragenter, dragleave, dragover, drop and dragend) and you must cancel dragover on every valid target. Data travels through event.dataTransfer as strings, and you can only read it inside the drop handler.

There is no keyboard path and no built-in way to animate the other cards out of the way. That is fine for dropping a file onto an upload area, but a sortable board quickly grows into a small library of its own.

How it works

  1. Drag a card within its column to reorder it, or across to another column to move it.
  2. No mouse? Tab to a card and use its move buttons to change position or column.
  3. Copy the dnd-kit sortable list first, then the kanban version that adds a group per column.
  4. Keep your board data as plain state keyed by column, and let onDragEnd produce the next copy.

Tips

Frequently asked questions

Is react-beautiful-dnd still a good choice?

Atlassian archived the react-beautiful-dnd repository and recommends its successor, Pragmatic drag and drop. For new React work, dnd-kit or Pragmatic drag and drop are the actively maintained options to compare.

What is the difference between @dnd-kit/core and @dnd-kit/react?

@dnd-kit/core with @dnd-kit/sortable is the long-running React package pair. @dnd-kit/react is the newer adapter built on a framework-agnostic core that also powers the Vue, Svelte and Solid adapters. New code can start with @dnd-kit/react as the current docs do.

Why does my drop event never fire with the HTML API?

A drop target must call event.preventDefault() in its dragover handler. Without that call the browser treats the element as an invalid target and the drop event is never sent.

How do I make drag and drop accessible?

Make each card focusable, offer a keyboard way to pick up, move and drop it, and announce the result in a live region. dnd-kit ships a keyboard sensor and accessibility support out of the box; with the native API you add all of it yourself.

Related: React table examples, JavaScript event listeners, Drag and drop website editing.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap