Kanban Board
Preview: Board
Board with draggable cards between columns. Fully keyboard operable via React Aria drag and drop (focus a card, press Enter to start dragging, Tab/arrow to a target, Enter to drop).
Import
import { Kanban } from '@marbleui/ui'Props
<Kanban>
| Prop | Type | Default | Description |
|---|---|---|---|
| columns *required | KanbanColumn<C>[] | — | |
| onChange *required | (columns: KanbanColumn<C>[]) => void | — | |
| renderCard *required | (card: C) => ReactNode | — | |
| className | string | — |
Accessibility
Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Button · React Aria DropIndicator · React Aria GridList · React Aria GridListItem. Follow the primitive's docs for the full keyboard map.
Usage notes
Board of columns whose cards move by drag and drop, with a pointer or the keyboard. To reorder a single list use SortableList.
const [columns, setColumns] = useState<KanbanColumn<Issue>[]>(initialColumns)
<Kanban columns={columns} onChange={setColumns} renderCard={(issue) => <IssueCard issue={issue} />} />
- Controlled: every drop calls
onChangewith the newcolumnsarray; save the move there. A column is{ id, title, cards, dot? }, wheredotis a background class such as"bg-warning". - Each card needs a unique
idand atitle; the title is its accessible name and the plain-text drag data. - Keyboard: focus a card (or its drag handle) and press Enter, use Tab to change column and the arrow keys to pick a position, then Enter to drop or Escape to cancel. ⌘/Ctrl- or Shift-click selects several cards in a column to move together.
- Adding, editing or deleting cards and reordering columns aren’t built in.
Example source · show· hide
import { useState } from 'react'
import { Avatar } from '../avatar'
import { Badge } from '../badge'
import { Kanban, type KanbanColumn } from './kanban'
type Card = { id: string; title: string; label: string; tone: 'accent' | 'warning' | 'danger' | 'neutral'; owner: string; points: number }
const initial: KanbanColumn<Card>[] = [
{ id: 'todo', title: 'Todo', dot: 'bg-fg-subtle', cards: [
{ id: 'a', title: 'Audit color contrast', label: 'a11y', tone: 'accent', owner: 'Maya Chen', points: 2 },
{ id: 'b', title: 'Billing page empty state', label: 'design', tone: 'neutral', owner: 'Ada Lovelace', points: 1 },
] },
{ id: 'doing', title: 'In progress', dot: 'bg-warning', cards: [
{ id: 'c', title: 'Data grid column resizing', label: 'feature', tone: 'warning', owner: 'Charles Webb', points: 5 },
] },
{ id: 'review', title: 'In review', dot: 'bg-accent', cards: [
{ id: 'd', title: 'Fix drawer snap points on iOS', label: 'bug', tone: 'danger', owner: 'Grace Hopper', points: 3 },
{ id: 'e', title: 'Toast view transitions', label: 'feature', tone: 'warning', owner: 'Maya Chen', points: 2 },
] },
{ id: 'done', title: 'Done', dot: 'bg-success', cards: [] },
]
function Board() {
const [columns, setColumns] = useState(initial)
return (
<Kanban
columns={columns}
onChange={setColumns}
className="max-w-[56rem]"
renderCard={(c) => (
<div className="flex flex-col gap-2.5">
<span className="font-medium">{c.title}</span>
<div className="flex items-center gap-2">
<Badge tone={c.tone}>{c.label}</Badge>
<span className="text-xs text-fg-subtle tabular-nums">{c.points} pts</span>
<Avatar size="xs" name={c.owner} className="ml-auto" />
</div>
</div>
)}
/>
)
}
export const styles = { Board }Source: packages/ui/src/kanban/kanban.tsx
Catalog
- Category
- data-grid
- Priority
- Pro
- Primitive
- composed
- React Aria
- GridList + DnD hooks