Skip to content

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.

Layout & Display13 propsAccessibility notes

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.tsx
<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

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

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

renderColumnHeader

(column, { count }) => ReactNode

default: —

Replace the default title + count pill.

emptyColumnContent

ReactNode

default: —

Shown in a column with no cards.

Layout & positioning

columnWidth

number

default: 288

Column width in px.

isCompact

boolean

default: false

Tighter spacing.

Styling

classNames

{ base?, column?, columnHeader?, columnTitle?, columnCount?, list?, card?, footer? }

default: —

Slot classes.

Accessibility

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.