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.
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:
| Package | Version | Used for |
|---|---|---|
@dnd-kit/core | 6.3.1 | DndContext, sensors, DragOverlay, useDroppable |
@dnd-kit/sortable | 10.0.0 | SortableContext, useSortable, arrayMove |
@dnd-kit/utilities | 3.2.2 | CSS.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[]>.
| Part | What it does |
|---|---|
DndContext | One for the whole board, with closestCorners collision detection |
| Sensors | Mouse after 4px of movement; touch after a 150ms press (5px tolerance), so a swipe still scrolls the board; keyboard with sortableKeyboardCoordinates |
SortableContext | One per column, with the column's task keys and verticalListSortingStrategy |
useDroppable | Makes each column's list a drop target, so a task can be dropped in an empty column |
useSortable | Makes each card draggable; the transition is 150ms ease, or none under prefers-reduced-motion |
DragOverlay | Shows 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:
onDragStartremembers the board as it was.onDragOvermoves the task to another column as soon as it is over that column, so the cards there make room.onDragEndreorders within the column witharrayMove.onDragCancelruns 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
accessibilityprop,DndContextgetsscreenReaderInstructionswith these keys, andannouncementsthat 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 onlytabIndex,aria-roledescriptionandaria-describedbyfromuseSortable'sattributes, not itsrole="button". In your own lists, spread allattributesunless the element has a role to keep. - Under
prefers-reduced-motion, the cards and the overlay move without animation. The board checks this withuseReducedMotion()from@/hooks/use-media-query.
Related
- Next.js Drag and Drop with dnd kit: the same board in AdminKit for Next.js
- Bootstrap 5 drag and drop: the HTML version, with SortableJS
- Card: the cards of the task board
- Dropdowns: the column menus of the board
- Tasks demo
Last updated on