AdminKitDocs
Plugins

React Drag and Drop with dnd kit

React drag and drop in AdminKit PRO: how the task board sorts and moves cards between columns with dnd kit, by mouse, touch and keyboard.

Pro

This feature is only available in AdminKit PRO. Learn more.

Drag and drop lets people put things in order by moving them: tasks in a column, items in a list, cards on a board. The task board of AdminKit for React does this with dnd kit. You drag a task up or down within its column, or over to another column, with the mouse, a finger or the keyboard.

There is no wrapper component. The board uses dnd kit directly, and you use it the same way in your own components. AdminKit PRO installs three packages:

PackageVersionUsed for
@dnd-kit/core6.3.1DndContext, sensors, DragOverlay, useDroppable
@dnd-kit/sortable10.0.0SortableContext, useSortable, arrayMove
@dnd-kit/utilities3.2.2CSS.Translate.toString() for the drag transform

The docs cannot run dnd kit, so this page has code only. Try the board in the tasks demo.

Example

The simplest case is a list people can reorder. This one uses the same sensors and drag styles as the task board:

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

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

  return (
    <div
      ref={setNodeRef}
      className={cx("list-group-item cursor-grab", isDragging && "sortable-ghost")}
      style={{ transform: CSS.Translate.toString(transform), transition }}
      {...attributes}
      {...listeners}
    >
      {id}
    </div>
  );
}

export function SortableList() {
  const [items, setItems] = useState([
    "Write release notes",
    "Review pull request",
    "Update dependencies",
  ]);
  const sensors = useSensors(
    useSensor(MouseSensor, { activationConstraint: { distance: 4 } }),
    useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
  );

  const onDragEnd = ({ active, over }: DragEndEvent) => {
    if (over && active.id !== over.id) {
      setItems((current) =>
        arrayMove(current, current.indexOf(String(active.id)), current.indexOf(String(over.id)))
      );
    }
  };

  return (
    <DndContext
      id="sortable-list"
      sensors={sensors}
      collisionDetection={closestCorners}
      onDragEnd={onDragEnd}
    >
      <SortableContext items={items} strategy={verticalListSortingStrategy}>
        <div className="list-group">
          {items.map((item) => (
            <SortableItem key={item} id={item} />
          ))}
        </div>
      </SortableContext>
    </DndContext>
  );
}

.sortable-ghost fades the item being dragged to 40% opacity, and .cursor-grab gives it the grab cursor. Both classes are in AdminKit's Sass.

How the task board works

The board is in src/components/pages/apps/tasks/task-board.tsx, and its starting data come from src/mocks/tasks.ts. It keeps two pieces of state: the tasks by key, and the order of the task keys in each column, a Record<ColumnId, string[]>.

PartWhat it does
DndContextOne for the whole board, with closestCorners collision detection
SensorsMouse after 4px of movement; touch after a 150ms press (5px tolerance), so a swipe still scrolls the board; keyboard with sortableKeyboardCoordinates
SortableContextOne per column, with the column's task keys and verticalListSortingStrategy
useDroppableMakes each column's list a drop target, so a task can be dropped in an empty column
useSortableMakes each card draggable; the transition is 150ms ease, or none under prefers-reduced-motion
DragOverlayShows a copy of the card under the pointer (.sortable-drag), in a portal on document.body; no drop animation under reduced motion

Four handlers move the keys around:

  • onDragStart remembers the board as it was.
  • onDragOver moves the task to another column as soon as it is over that column, so the cards there make room.
  • onDragEnd reorders within the column with arrayMove.
  • onDragCancel runs when the user presses Escape. It puts the board back as it was at the start.

Vite builds a single-page app that only renders in the browser. So the board can pass document.body to createPortal for the overlay without checking that it exists.

Accessibility

  • The keyboard sensor makes every card usable without a mouse: Tab to a card, Space or Enter to pick it up, the arrow keys to move it within or between columns, Space or Enter to drop it and Escape to cancel.
  • Through its accessibility prop, DndContext gets screenReaderInstructions with these keys, and announcements that name the task and its place, such as "Picked up …, in To do, position 2 of 5."
  • The cards stay <article> elements. The board passes only tabIndex, aria-roledescription and aria-describedby from useSortable's attributes, not its role="button". In your own lists, spread all attributes unless the element has a role to keep.
  • Under prefers-reduced-motion, the cards and the overlay move without animation. The board checks this with useReducedMotion() from @/hooks/use-media-query.

Last updated on

On this page