Data
KanbanBoard
A columns-of-cards board with pointer drag-and-drop (reorder within a column, move across columns, reorder columns) and a required keyboard alternative — built on the exact same Pointer Events technique as Scheduler's drag-to-reschedule, not a second drag mechanism.
KanbanBoardPreview
To do2
Write design docdocs
Set up project scaffoldingsetup
In progress11/2
Build KanbanBoard componentDrag-and-drop plus a keyboard-accessible alternative.feature
Done1
Add Scheduler componentfeature
Usage
tsx
import { useState } from "react";
import { KanbanBoard, type KanbanColumn } from "@vesture/react";
function Example() {
const [columns, setColumns] = useState<KanbanColumn[]>([
{
id: "todo",
title: "To do",
cards: [{ id: "1", title: "Write design doc", tags: ["docs"] }],
},
{
id: "in-progress",
title: "In progress",
wipLimit: 3,
cards: [
{
id: "2",
title: "Build KanbanBoard",
assignee: { name: "Ada Lovelace" },
},
],
},
{ id: "done", title: "Done", cards: [] },
]);
return (
<KanbanBoard
columns={columns}
onColumnsChange={setColumns}
onCardClick={(card, columnId) => console.log(card, columnId)}
onAddCard={(columnId) =>
setColumns((prev) =>
prev.map((col) =>
col.id === columnId
? { ...col, cards: [...col.cards, { id: crypto.randomUUID(), title: "New card" }] }
: col
)
)
}
onAddColumn={() =>
setColumns((prev) => [
...prev,
{ id: crypto.randomUUID(), title: "New column", cards: [] },
])
}
/>
);
}Behavior
- Fully controlled: KanbanBoard never mutates your columns array. Every structural change — add/remove card or column, drag reorder, keyboard move — calls onColumnsChange with the fully updated array; you decide whether/how to apply it, same philosophy as every other component here.
- Card drag: press and hold a card to start dragging it (within its column or across columns). The original dims in place, a small floating preview follows the pointer, and an insertion line shows exactly where it will land — computed from the pointer's X (which column) and Y (position among that column's cards). Releasing at the same spot it started fires onCardClick instead of a reorder.
- Column drag: press and hold a column's header (not its card list) to reorder columns horizontally, same insertion-line treatment one level up.
- Required keyboard alternative — not optional: focus a card and use ArrowUp/ArrowDown to move it within its column, or ArrowLeft/ArrowRight to move it to the same position in the adjacent column. Focus a column header and use Shift+ArrowLeft/Shift+ArrowRight to reorder columns. Every keypress commits immediately via onColumnsChange and is announced through an aria-live region for screen readers.
- wipLimit is informational, never enforced: a column over its limit shows a warning-colored count badge, and dragging a card over a column that would put it over the limit shows that same warning treatment while hovering — but the drop is never blocked.
- renderCard fully overrides a card's content (falls back to a default layout using Card, Badge for tags, and Avatar for the assignee) — the card's focusability, drag handling, and keyboard reordering all live on the wrapping element regardless, so a custom render only needs to supply presentation content.
- No built-in virtualization: plain rendering was checked at 60 cards in a single column and held up fine, so — unlike DataGrid/TreeView — there's no scrollTop-driven windowing here. Revisit only if a real column is an order of magnitude larger than that.
Props
KanbanCard
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | — | Stable identity, used for drag tracking, focus retention, and React keys. |
| title | string | — | Required. |
| description | string | — | Shown under the title in the default card layout. |
| tags | string[] | — | Rendered as Badges in the default card layout. |
| assignee | { name: string; avatarUrl?: string } | — | Rendered as an Avatar in the default card layout. |
| data | unknown | — | Passthrough slot for your own metadata — KanbanBoard never reads it. |
KanbanColumn
| Prop | Type | Default | Description |
|---|---|---|---|
| id / title | string | — | Required. |
| cards | KanbanCard[] | — | Required. |
| wipLimit | number | — | Optional work-in-progress cap, shown as a count badge — visually flagged when exceeded, never blocking (see behavior above). |
KanbanBoardProps
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | KanbanColumn[] | — | Required. |
| onColumnsChange | (columns: KanbanColumn[]) => void | — | Required for any interactivity — add/remove, drag, and keyboard reordering are all gated on this being passed; with no onColumnsChange, the board renders read-only. |
| onCardClick | (card: KanbanCard, columnId: string) => void | — | Fires on a plain click (or a drag released at its start position) — not fired for a click that ends an actual drag. |
| onAddCard | (columnId: string) => void | — | Fires when a column's "+ Add card" affordance is used. KanbanBoard has no built-in card-creation form — wire up your own Modal/Drawer and call onColumnsChange once you have the new card. |
| onAddColumn | () => void | — | Fires when the "+ Add column" affordance is used. |
| renderCard | (card: KanbanCard) => ReactNode | — | Overrides the default card layout entirely. |