Kanban
kanbanOrganizes draggable cards across status columns with controlled movement callbacks.
Usage
Basic usage
Controlled items + columns bucket display. getId takes the stable primary key, and getColumnId takes the column to which the card currently belongs.
Login page revision
Lin Wanqing
Customer List Filter
Zhou Mingyuan
Order details drawer
Chen Ce
Business Opportunity Board Drag and Drop
High sensitivity
Workbench Diagram
Su Xiao
<Kanban
items={cards}
columns={[
{ id: "backlog", title: "To be planned" },
{ id: "todo", title: "To be developed" },
{ id: "doing", title: "In Progress" },
{ id: "done", title: "Completed" },
]}
getId={(c) => c.id}
getColumnId={(c) => c.status}
onMove={() => {}}
renderItem={(c) => (
<div className="rounded-[var(--radius)] border border-border bg-surface p-3 shadow-sm">
<p className="text-sm font-medium text-foreground">{c.title}</p>
<p className="mt-1 text-xs text-muted">{c.owner}</p>
</div>
)}
/>Drag across columns (controlled reconciliation)
The component does not directly change the business field, but only returns onMove; the consumer changes status accordingly and inserts back the target column according to toIndex. Supports pointer and keyboard (focus card Space to grab / arrow keys to move / Space to put down).
Login page revision
Lin Wanqing
Customer List Filter
Zhou Mingyuan
Order details drawer
Chen Ce
Business Opportunity Board Drag and Drop
High sensitivity
Workbench Diagram
Su Xiao
const [cards, setCards] = useState(INITIAL);
<Kanban
items={cards}
columns={columns}
getId={(c) => c.id}
getColumnId={(c) => c.status}
onMove={(e) => setCards((prev) => applyMove(prev, e))}
renderItem={(c) => <Card title={c.title} owner={c.owner} />}
/>Custom column header statistics
renderColumnHeader After getting this column of cards, you can aggregate the number of items/progress, etc.
Login page revision
Lin Wanqing
Customer List Filter
Zhou Mingyuan
Order details drawer
Chen Ce
Business Opportunity Board Drag and Drop
High sensitivity
Workbench Diagram
Su Xiao
<Kanban
items={cards}
columns={columns}
getId={(c) => c.id}
getColumnId={(c) => c.status}
onMove={(e) => setCards((prev) => applyMove(prev, e))}
renderColumnHeader={(col, its) => (
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-semibold text-foreground">{col.title}</span>
<Tag tone="neutral" size="sm">{its.length}</Tag>
</div>
)}
renderItem={(c) => <Card title={c.title} owner={c.owner} />}
/>When to use
Use Kanban for controlled deal, task, or ticket movement across workflow columns. Use Sortable for one list or Flow for a connected node canvas.
Import
import { Kanban, resolveKanbanMove } from "@hulianui/ui"Props
KanbanProps<T> is generic.
| Name | Type | Default | Description |
|---|---|---|---|
| columns * | KanbanColumn[] | — | Ordered { id, title, header?, footer? } definitions. |
| items * | T[] | — | Controlled cards grouped by getColumnId; array order defines order within a column. |
| getId * | (item: T) => string | — | Globally stable unique card id. |
| getColumnId * | (item: T) => string | — | Current column id. |
| className | string | — | Board class name. |
| columnClassName | string | — | Class applied to each column. |
Events
| Event | Type | Description |
|---|---|---|
| onMove * | (e: KanbanMoveEvent) => void | Reports a completed move; the consumer updates business fields and state. |
Slots
| Slot | Type | Description |
|---|---|---|
| renderItem * | (item: T, state: { dragging: boolean }) => ReactNode | Renders one card. |
| renderColumnHeader | (column: KanbanColumn, items: T[]) => ReactNode | Custom header with access to column cards; defaults to header or title. |
KanbanMoveEvent is { id, fromColumn, toColumn, toIndex }; toIndex is calculated after removing the dragged card from its target column.
Example
const [cards, setCards] = useState(initial);
<Kanban
items={cards}
columns={columns}
getId={(c) => c.id}
getColumnId={(c) => c.status}
onMove={(e) => setCards((prev) => applyMove(prev, e))}
renderColumnHeader={(col, its) => <div>{col.title}<Tag>{its.length}</Tag></div>}
renderItem={(c) => <Card>{c.title}</Card>}
/>Usage notes
- Kanban never mutates cards. Update the column field and insert at
toIndex, or useresolveKanbanMove. toIndexassumes the dragged card has already been removed, including for same-column moves.- IDs must be globally unique. Interactive descendants also rely on the guard described in [[dnd-kit-draggable-container-guard-interactive-children]].
- Empty columns show built-in Chinese
"\u62d6\u62fd\u5361\u7247\u5230\u6b64", meaning “Drag a card here.”
Related
The empty-column message follows ConfigProvider.
Table · Book3D · ProTable · PricingTable · JsonViewer · EditableTable