Board component
Board is a data-driven Kanban board with dependency-free pointer, touch, and keyboard drag-and-drop, per-column card limits, and live-region announcements for every move.
Playground
Change a prop on the right and the preview updates. Switch to Code to copy what you see.
Recipes
Copy-paste patterns for the ways Board actually gets used — each one a real, self-contained example.
Kanban board
Board takes columns plus a flat items array — getItemColumn places each item, renderCard draws it, and onItemMove reports drag/keyboard moves as { itemId, to }.
const [tasks, setTasks] = useState(initialTasks);
<Board
aria-label="Sprint board"
columns={[
{ id: "todo", title: "Backlog" },
{ id: "doing", title: "Active" },
{ id: "done", title: "Shipped" },
]}
items={tasks}
getItemId={(t) => t.id}
getItemColumn={(t) => t.status}
onItemMove={({ itemId, to }) => moveTask(itemId, to.columnId, to.index)}
renderCard={(t) => (
<Card shadow="xs" isHoverable>
<CardBody>{t.title}</CardBody>
</Card>
)}
/>Column WIP limits
A column's limit caps how many cards it accepts. A move from another column that would exceed it is refused, and keyboard users hear that the column can't take the card. Reordering inside a full column still works.
columns={[
{ id: "todo", title: "Queued" },
{ id: "doing", title: "In review", color: "warning", limit: 3 },
{ id: "done", title: "Approved", color: "success" },
]}Example
<Board
columns={[
{ id: "todo", title: "To do", cards: todo },
{ id: "doing", title: "In progress", cards: doing, limit: 3 },
{ id: "done", title: "Done", cards: done },
]}
onCardMove={handleMove}
/>Props
The full public API for Board. Every prop is stable and intentionally typed.
Data
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | BoardColumnData[] | — | { id, title, limit?, isDropDisabled?, color?, headerActions?, footer? }. |
| items | Item[] | — | Flat list, grouped by column at render. |
| getItemId | (item) => string | — | Stable id. |
| getItemColumn | (item) => string | — | Which column the item is in. |
| renderCard | (item, ctx) => ReactNode | — | ctx: { isDragging, isGrabbed, column, index }. |
columns
BoardColumnData[]
default: —
{ id, title, limit?, isDropDisabled?, color?, headerActions?, footer? }.
items
Item[]
default: —
Flat list, grouped by column at render.
getItemId
(item) => string
default: —
Stable id.
getItemColumn
(item) => string
default: —
Which column the item is in.
renderCard
(item, ctx) => ReactNode
default: —
ctx: { isDragging, isGrabbed, column, index }.
Behavior & states
| Prop | Type | Default | Description |
|---|---|---|---|
| onItemMove | (move) => void | — | move: { item, itemId, from: {columnId,index}, to: {columnId,index} }. Apply it to your state. |
| isDragDisabled | boolean | false | Read-only board — no dragging. |
onItemMove
(move) => void
default: —
move: { item, itemId, from: {columnId,index}, to: {columnId,index} }. Apply it to your state.
isDragDisabled
boolean
default: false
Read-only board — no dragging.
Appearance
| Prop | Type | Default | Description |
|---|---|---|---|
| renderColumnHeader | (column, { count }) => ReactNode | — | Replace the default title + count pill. |
| emptyColumnContent | ReactNode | — | Shown in a column with no cards. |
renderColumnHeader
(column, { count }) => ReactNode
default: —
Replace the default title + count pill.
emptyColumnContent
ReactNode
default: —
Shown in a column with no cards.
Layout & positioning
| Prop | Type | Default | Description |
|---|---|---|---|
| columnWidth | number | 288 | Column width in px. |
| isCompact | boolean | false | Tighter spacing. |
columnWidth
number
default: 288
Column width in px.
isCompact
boolean
default: false
Tighter spacing.
Styling
| Prop | Type | Default | Description |
|---|---|---|---|
| classNames | { base?, column?, columnHeader?, columnTitle?, columnCount?, list?, card?, footer? } | — | Slot classes. |
classNames
{ base?, column?, columnHeader?, columnTitle?, columnCount?, list?, card?, footer? }
default: —
Slot classes.
Accessibility
| Prop | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | Required — names the board. |
aria-label
string
default: —
Required — names the board.
Accessibility
- Cards are focusable; Space picks a card up, arrow keys move it between columns and positions, Space drops it, Escape cancels.
- Every pick-up, move, and drop is announced through an `aria-live` region.
- Column card limits are announced when reached; the drop is blocked, not silently lost.