Kanban
PronewA drag-and-drop board of sortable columns and cards, operable by pointer, touch and keyboard.
Installation
npx shadcn@latest add @blank-pro/kanbanNeeds a Blank Pro licence key and the @blank-pro registry. Set up Pro access
Usage
import {
Kanban,
KanbanBoard,
KanbanColumn,
KanbanColumnHandle,
KanbanItem,
KanbanItemHandle,
KanbanOverlay,
} from "@/components/ui/kanban"const [columns, setColumns] = React.useState({
backlog: [{ id: "1", title: "Write the theming guide" }],
done: [],
})
<Kanban
value={columns}
onValueChange={setColumns}
getItemValue={(task) => task.id}
getItemLabel={(task) => task.title}
>
<KanbanBoard>
{Object.entries(columns).map(([column, tasks]) => (
<KanbanColumn key={column} value={column}>
{tasks.map((task) => (
<KanbanItem key={task.id} value={task.id} asHandle>
<TaskCard task={task} />
</KanbanItem>
))}
</KanbanColumn>
))}
</KanbanBoard>
<KanbanOverlay>
{({ value, variant }) => …}
</KanbanOverlay>
</Kanban>value is the whole board: column keys in display order, each mapped to its
items in order. Every move — within a column, across columns, or of a column
itself — comes back through onValueChange as a new map, so persisting the
board is one setState or one request. Items can be strings or objects; for
objects, getItemValue returns the id.
KanbanOverlay renders the copy that follows the pointer, so the card can
leave its column without being clipped. Its render prop gets the active id and
whether it is a column or an item; render the same card you render on the
board and it is styled as lifted. The original stays behind as a dashed slot.
Pressing Escape mid-drag puts the card back where it started, even after it has
crossed into another column.
Cards are draggable by keyboard: focus a card or handle, press Space, move with
the arrow keys and press Space again. Screen readers hear where it is at every
step — "Focus ring disappears in dark mode is now at position 2 of 3 in Done" —
when you name things with getItemLabel and getColumnLabel.
asHandle makes a whole card or column the drag handle. For cards with their
own buttons or checkboxes, put a KanbanItemHandle inside instead, as below.
Examples
Vertical, with handles
orientation="vertical" stacks the columns — sections of a list here — and
KanbanItemHandle leaves the rest of each row free for its own controls.
API Reference
Kanban
Takes DndContext props (sensors, modifiers, onDragStart, onDragEnd…)
except collisionDetection, plus:
| Prop | Type | Default |
|---|---|---|
value | Record<UniqueIdentifier, T[]> | — |
defaultValue | Record<UniqueIdentifier, T[]> | {} |
onValueChange | (columns) => void | — |
onMove | (event & { activeIndex, overIndex }) => void | — |
getItemValue | (item: T) => UniqueIdentifier | the item itself |
getItemLabel | (item: T) => string | the id |
getColumnLabel | (column) => string | the key |
orientation | "horizontal" | "vertical" | "horizontal" |
onMove, when given, replaces onValueChange for reorders that finish on
drop — useful when the server decides the order.
KanbanBoard
div props, plus asChild.
KanbanColumn
| Prop | Type | Default |
|---|---|---|
value | UniqueIdentifier | — |
asHandle | boolean | false |
asChild | boolean | false |
disabled | boolean | false |
Carries data-dragging while it is being moved and data-drop-target while
it holds the card being dragged.
KanbanItem
| Prop | Type | Default |
|---|---|---|
value | UniqueIdentifier | — |
asHandle | boolean | false |
asChild | boolean | false |
disabled | boolean | false |
children | ReactNode | ({ dragging, overlay }) => ReactNode | — |
Carries data-dragging on the slot left behind and data-overlay on the copy
in KanbanOverlay.
KanbanColumnHandle, KanbanItemHandle
button props, plus asChild.
KanbanOverlay
DragOverlay props, plus:
| Prop | Type | Default |
|---|---|---|
container | Element | DocumentFragment | null | document.body |
children | ReactNode | ({ value, variant }) => ReactNode | — |
With prefers-reduced-motion, the drop animation is skipped and the lifted
copy does not tilt.