Kanban
A drag-and-drop board for moving cards between columns, built on dnd-kit. State is fully controlled through onDataChange.
Preview
Keep the cards in a single flat array where each item points at its column. Drag a card between columns – the provider updates the array and hands it back via onDataChange.
const [tasks, setTasks] = useState(demoTasks)
<KanbanProvider columns={columns} data={tasks} onDataChange={setTasks}>
{(column) => (
<KanbanBoard id={column.id} key={column.id}>
<KanbanHeader>{column.name}</KanbanHeader>
<KanbanCards id={column.id}>
{(task) => (
<KanbanCard
key={task.id}
id={task.id}
name={task.name}
column={column.id}
/>
)}
</KanbanCards>
</KanbanBoard>
)}
</KanbanProvider>Usage
import {
KanbanBoard,
KanbanCard,
KanbanCards,
KanbanHeader,
KanbanProvider,
type DragEndEvent,
} from "@/components/kibo-ui/kanban"const columns = [
{ id: "todo", name: "Todo" },
{ id: "done", name: "Done" },
]
const [items, setItems] = useState([
{ id: "1", name: "Write docs", column: "todo" },
{ id: "2", name: "Review PR", column: "done" },
])
<KanbanProvider columns={columns} data={items} onDataChange={setItems}>
{(column) => (
<KanbanBoard id={column.id} key={column.id}>
<KanbanHeader>{column.name}</KanbanHeader>
<KanbanCards id={column.id}>
{(item) => (
<KanbanCard key={item.id} id={item.id} name={item.name} column={column.id} />
)}
</KanbanCards>
</KanbanBoard>
)}
</KanbanProvider>Custom card content
KanbanCard renders its name by default; pass children instead to lay out your own card body – the drag overlay clones whatever you render.
<KanbanCard key={task.id} id={task.id} name={task.name} column={column.id}>
<div className="flex items-start justify-between gap-2">
<p className="m-0 flex-1 text-sm font-medium">{task.name}</p>
<Avatar className="h-4 w-4 shrink-0">
<AvatarImage src={task.owner.image} />
<AvatarFallback>{task.owner.initials}</AvatarFallback>
</Avatar>
</div>
<p className="m-0 text-xs text-muted-foreground">{task.dueDate}</p>
</KanbanCard>Drag events
Beyond onDataChange, the provider forwards dnd-kit's onDragStart, onDragOver and onDragEnd – useful for persisting the move once the drag settles.
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over) return
saveOrder(active.id, over.id)
}
<KanbanProvider
columns={columns}
data={items}
onDataChange={setItems}
onDragEnd={handleDragEnd}
>
{...}
</KanbanProvider>API reference
The board is generic: card data extends { id: string; name: string; column: string } and columns extend { id: string; name: string } – any extra fields flow through to the render props untouched. KanbanProvider shares columns, data and the active card through context, so KanbanCards and KanbanCard must render inside it.
KanbanProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | C[] extends { id, name }[] | – | The board's columns, rendered in order. |
| data | T[] extends { id, name, column }[] | – | Flat array of all cards; each card's column field decides where it appears. |
| children | (column: C) => ReactNode | – | Render prop invoked once per column – return a KanbanBoard. |
| onDataChange | (data: T[]) => void | – | Called with the reordered array while dragging over and when a drag ends – wire it to your state setter. |
| onDragStart / onDragOver / onDragEnd | (event) => void | – | dnd-kit event callbacks, forwarded after the internal handlers run. |
| className | string | – | Extra classes for the column grid (auto-cols-fr grid-flow-col with gap-4). |
KanbanBoard
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | – | Column id – registered as a droppable zone; a primary ring appears while a card hovers over it. |
KanbanCards
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | – | Column id – filters the provider's data to this column and creates the sortable context. |
| children | (item: T) => ReactNode | – | Render prop invoked for each card in the column – return a KanbanCard. |
KanbanCard
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | – | Unique card id used by the sortable context. |
| name | string | – | Card label – rendered as the default body when no children are passed, and read aloud in drag announcements. |
| column | string | – | The column the card currently belongs to. |
| children | ReactNode | – | Custom card body, replacing the default name text. |
| Component | Description |
|---|---|
| KanbanBoard | Droppable column surface on bg-secondary – rounded, divided rows, ring-primary highlight while a card is over it. |
| KanbanHeader | Column title row (font-semibold, p-2). Plain div – put a color dot, count or menu inside. |
| KanbanCards | Scrollable, sortable card stack for one column, wrapped in a ScrollArea. |
| KanbanCard | Draggable Card. While dragging, the in-place card dims and a clone follows the pointer in a DragOverlay portal. |
Accessibility
The provider wires up mouse, touch and keyboard sensors, so cards can be picked up and moved from the keyboard. It also supplies dnd-kit Announcements for every drag phase – picked up, dragged over, dropped, cancelled – naming the card and column for screen readers.