Tutorial · September 26, 2026 · 4 min read
How to build a drag and drop Kanban board in React
To build a drag and drop Kanban board in React, install ReactFrame's free Kanban Board with npx shadcn@latest add @reactframe/kanban-board, then pass your columns and cards. Dragging runs on pointer events, so it works with a mouse, a pen or a finger, and the onCardsChange callback hands you every card's new column and order to save.
A Kanban board looks simple and turns out to be fiddly: pointer events, drop targets, reordering inside a column, auto-scrolling, and a card that follows the cursor without jank. This tutorial uses ReactFrame's free Kanban Board, which handles the dragging, so you only bring the data.
1. Install
npx shadcn@latest add @reactframe/kanban-board
It has no drag and drop dependency; the dragging is built on pointer events, so it works with a mouse, a pen or a finger, and the board scrolls on its own when you drag a card near its edge.
2. Columns and cards
import { KanbanBoard, type KanbanCard } from "@/components/kanban-board";
const columns = [
{ id: "todo", title: "To do", accentColor: "#9a9a96" },
{ id: "doing", title: "In progress", accentColor: "#c98a3f" },
{ id: "done", title: "Done", accentColor: "#5fa874" },
];
const cards: KanbanCard[] = [
{ id: "1", title: "Write launch post", tag: "Content", tagColor: "#6f7fbf", priority: "high", columnId: "todo" },
{ id: "2", title: "Fix checkout bug", tag: "Engineering", tagColor: "#d44c3a", priority: "urgent", columnId: "doing" },
{ id: "3", title: "Pick pricing", tag: "Strategy", priority: "medium", columnId: "done", avatars: ["/team/ada.jpg"] },
];
export function Board() {
return <KanbanBoard boardTitle="Launch" columns={columns} cards={cards} />;
}
Each card belongs to a column through columnId. Optional fields add detail: a tag with its tagColor, a priority ("urgent", "high", "medium" or "low"), a note, and avatars (image URLs, up to four shown).
3. Save every move
onCardsChange runs after each drop with every card in its new column and order:
"use client";
import * as React from "react";
import { KanbanBoard, type KanbanCard } from "@/components/kanban-board";
export function Board({ initialCards }: { initialCards: KanbanCard[] }) {
const [cards, setCards] = React.useState(initialCards);
return (
<KanbanBoard
columns={columns}
cards={cards}
onCardsChange={async (next) => {
setCards(next);
await fetch("/api/cards", { method: "PUT", body: JSON.stringify(next) });
}}
/>
);
}
Storing the whole list is the simplest approach for small boards. For bigger ones, compare next with the previous list and save only the card whose columnId or position changed.
4. Look and feel
defaultTheme("dark"or"light") andshowThemeToggle.showCountsshows the number of cards on each column header.- Separate light and dark colours:
backgroundColor,inkColor,mutedColor,surfaceColorand theirdark...versions.
Tips
- Keep card
ids stable (database ids, not array indexes), or cards can jump after a save. - On phone widths the columns stack vertically, so a four or five column board still reads well on mobile.
Related
See more app building blocks in the Dashboard kit, including the Data Table for list views of the same data.

Ahmet Loca · Product designer
Hi, I'm Ahmet. I have over four years of experience creating intuitive, user-focused designs for digital products, and ReactFrame is where that work becomes components you can use in your own.