KanbanBoard

<KanbanBoard> renders records from the parent <List> as a drag-and-drop Kanban board. Records are grouped into columns by an enum field (status, stage, priority, etc.). Dragging a card to another column calls useUpdate optimistically, so the UI updates immediately and rolls back on server error.

Installation

pnpm dlx shadcn@latest add @shadmin/kanban-board

Usage

import { List, KanbanBoard } from "@/components/admin";
 
const TaskList = () => (
  <List perPage={500}>
    <KanbanBoard
      groupBy="status"
      columns={[
        { id: "todo", label: "To do" },
        { id: "doing", label: "In progress" },
        { id: "done", label: "Done" },
      ]}
      titleSource="title"
      descriptionSource="description"
    />
  </List>
);

Each column receives records whose groupBy field matches the column's id. Records with unrecognized values are silently ignored. Columns with no matching records render a "No items" placeholder that still accepts drops.

Props

PropRequiredTypeDefaultDescription
groupByRequiredstringField name whose enum value buckets records into columns.
columnsRequiredArray<{ id; label; className? }>Column definitions in display order.
titleSourceOptionalstringrecordRepresentationField used as the card title. Falls back to the resource's recordRepresentation when omitted.
descriptionSourceOptionalstringOptional secondary line rendered below the title.
cardRendererOptional(record) => ReactNodeDefault cardFully custom card. When provided, titleSource and descriptionSource are ignored.
onCardClickOptional(record) => voidNavigate to show pageClick handler for cards. Defaults to navigate("/<resource>/<id>/show").

columns

Each entry in columns is an object with:

KeyTypeRequiredDescription
idstringYesMust match the enum value stored in the groupBy field.
labelstringYesHuman-readable column header.
classNamestringNoExtra CSS classes applied to the column wrapper element.

cardRenderer

Override the default card layout with a custom renderer. The function receives the raw record and must return a ReactNode:

<KanbanBoard
  groupBy="status"
  columns={columns}
  cardRenderer={(record) => (
    <div className="flex items-center gap-2">
      <PriorityBadge priority={record.priority} />
      <span>{record.title}</span>
    </div>
  )}
/>

Drag behavior

The board wraps all columns in a single @dnd-kit/core <DndContext>. Each card is a useDraggable node (aria-roledescription="draggable"). Each column is a useDroppable node. When a card is released over a column, the column highlights with a primary ring.

On a successful drop the component calls:

update(
  resource,
  {
    id: record.id,
    data: { [groupBy]: targetColumnId },
    previousData: record,
  },
  { mutationMode: "optimistic" },
);

Optimistic update

mutationMode: "optimistic" means the record moves to the target column immediately in the UI. If the server returns an error, react-admin rolls back the change and restores the record to its previous column. No additional configuration is required.

Pagination note

Kanban boards work best with all records loaded at once. Use perPage={500} (or a value higher than your expected record count) on the parent <List> to avoid pagination cutting off cards:

<List perPage={500}>
  <KanbanBoard ... />
</List>