Marble UI
← Components

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>

PropTypeDefaultDescription
columns *requiredKanbanColumn<C>[]—
onChange *required(columns: KanbanColumn<C>[]) => void—
renderCard *required(card: C) => ReactNode—
classNamestring—

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 onChange with the new columns array; save the move there. A column is { id, title, cards, dot? }, where dot is a background class such as "bg-warning".
  • Each card needs a unique id and a title; 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
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