Kanban

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.

Planned
In Progress
Done
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

PropTypeDefaultDescription
columnsC[] extends { id, name }[]–The board's columns, rendered in order.
dataT[] 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.
classNamestring–Extra classes for the column grid (auto-cols-fr grid-flow-col with gap-4).

KanbanBoard

PropTypeDefaultDescription
idstring–Column id – registered as a droppable zone; a primary ring appears while a card hovers over it.

KanbanCards

PropTypeDefaultDescription
idstring–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

PropTypeDefaultDescription
idstring–Unique card id used by the sortable context.
namestring–Card label – rendered as the default body when no children are passed, and read aloud in drag announcements.
columnstring–The column the card currently belongs to.
childrenReactNode–Custom card body, replacing the default name text.
ComponentDescription
KanbanBoardDroppable column surface on bg-secondary – rounded, divided rows, ring-primary highlight while a card is over it.
KanbanHeaderColumn title row (font-semibold, p-2). Plain div – put a color dot, count or menu inside.
KanbanCardsScrollable, sortable card stack for one column, wrapped in a ScrollArea.
KanbanCardDraggable 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.

Search documentation

Search the component docs