Skip to content
ilinxa/pro-ui

Kanban Board

alphav0.5.0

Drag-and-drop kanban board with swimlanes, tinted columns, per-column rules, and a renderer registry that hosts any card type.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-05Author: ilinxa

Context

Use when you need a Trello/Linear/JIRA-style board for tracking work, pipelines, or stage transitions. The renderer-registry pattern lets a single column mix the lightweight built-in `kanban-card`, the `kanban-note` annotation, and any rich card from elsewhere in this registry — all as siblings in an ordered, JSON-serializable item list. CRUD affordances are opt-in via callbacks; DnD via @dnd-kit (touch + keyboard accessible).

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/kanban-board

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/kanban-board-fixtures

Preview

To do3
Platform
Wire OAuth flow for new SDK

Validate against the staging IDP; fall back to existing token endpoint.

authplatform
  • due:May 12
ALBY
Reminder

Coordinate with the auth team on the new session shape before merging.

Product
drag
In progress2 / 3
Platform
drag
Product
Empty-state polish

Tighten spacing on the notifications panel.

design
CN
Review2
Platform
Cache eviction policy

Decide between LRU and ARC for the new session cache layer.

perfinfra
  • due:May 9
BY
Product
drag
Done2
Platform
Postmortem ready

Posted in #infra-incidents — link in the action items list.

Product
Empty-state illustrations
design
FE

Demo source

demo.tsxtsx

Usage

When to use

Reach for KanbanBoard when you need a column-based board with drag-and-drop reordering, optional swimlanes, optional CRUD, and the flexibility to host any card from this registry (or your own custom components) as first-class items in any column.

Key concepts

  • Items are pure JSON. A column's items[] is an array of { id, rendererId, data, swimlaneId?, locked? } records. The board never holds JSX in its data layer.
  • Renderers are pluggable. Two ship built-in (kanbanCardRenderer, kanbanNoteRenderer); register more via the renderers prop. Each declares an id and a render(data, ctx).
  • Drag works for items and columns. Items reorder within a column, move across columns, and (when swimlanes are provided) move across swimlane cells. Column headers themselves are draggable for reorder.
  • CRUD is opt-in. Pass onItemCreateand an inline "+ Add" row appears under each column. Same pattern for edit, delete, and column CRUD callbacks.

Basic example

import { KanbanBoard } from "@/components/kanban-board";
import { kanbanCardRenderer } from "@/components/kanban-board/parts/kanban-card";
import { kanbanNoteRenderer } from "@/components/kanban-board/parts/kanban-note";

export function Example() {
  return (
    <KanbanBoard
      renderers={[kanbanCardRenderer, kanbanNoteRenderer]}
      defaultData={{
        columns: [
          {
            id: "todo",
            title: "To do",
            items: [
              { id: "c1", rendererId: "kanban-card", data: { title: "Wire auth flow" } },
              { id: "n1", rendererId: "kanban-note", data: { title: "Reminder", body: "Coordinate with infra." } },
            ],
          },
          { id: "doing", title: "In progress", color: "lime", items: [] },
          { id: "done",  title: "Done",        color: "emerald", items: [], allowReorder: false },
        ],
      }}
    />
  );
}

Movement controls

  • column.allowReorder: false — items cannot reorder within this column.
  • column.allowIncoming: false — items cannot be dropped into this column from elsewhere.
  • column.allowOutgoing: false — items cannot leave this column.
  • column.acceptsRendererIds: [...] — only host the listed renderer kinds.
  • item.locked: true — pin an individual item; it cannot be dragged anywhere.
  • readOnly at the board level kills all DnD and CRUD affordances; items remain clickable.

Mixing rich cards (renderer adapter)

Any sibling registry component can be plugged in as a third renderer with all of its features intact. The demo wires <CardTree> from @ilinxa/card-tree — same pattern works for any rich card you author. Set dragHandle: "header" for renderers that own internal pointer interactions so the kanban grip appears on top and the body stays interactive:

import { CardTree, type CardTreeJsonNode } from "@ilinxa/card-tree";

function makeCardTreeRenderer(
  onChange: (id: string, next: CardTreeJsonNode) => void,
): KanbanCardRenderer<CardTreeJsonNode> {
  return {
    id: "card-tree",
    label: "Card tree",
    dragHandle: "header",   // ← thin grip strip; body stays interactive
    render: (data, ctx) => (
      <CardTree
        key={ctx.itemId}
        defaultValue={data}
        editable
        onChange={(tree) => onChange(ctx.itemId, tree)}
      />
    ),
  };
}

<KanbanBoard
  renderers={[kanbanCardRenderer, kanbanNoteRenderer, makeCardTreeRenderer(updateData)]}
  defaultData={{ /* items reference rendererId: "kanban-card" | "kanban-note" | "card-tree" */ }}
/>

dragHandle modes: "shell" (default) makes the whole card the drag activator — right for plain content cards. "header" renders a small grip strip on top and leaves the body fully interactive — right for renderers with click-to-edit fields, embedded inputs, or their own internal DnD.

Pre-built renderer — todo items: @ilinxa/task-card exports a ready-to-use taskCardKanbanRenderer (typed as KanbanCardRenderer<TaskItem>, dragHandle: "header") — no factory wrapper needed. Drop it directly into renderers={[...]} and give each item rendererId: "task-card" with a TaskItem-shaped data payload. See the task-card detail page for the live kanban demo + the full code recipe.

Keyboard

  • Tab cycles focus through items and column headers.
  • Space on a focused item lifts it (DnD mode); arrow keys move; Space drops; Escape cancels.
  • Enter on a focused item fires onItemClick.

Features

  • Renderer registry — items are pure JSON, the board delegates rendering by id
  • Two built-in renderers: kanban-card (title + meta + tags + assignees) and kanban-note (title + body)
  • Pluggable card-tree adapter pattern — wrap any sibling component (e.g. card-tree) as a renderer with full feature passthrough
  • Per-renderer dragHandle mode — `shell` (whole-card grab) or `header` (top grip strip; body stays interactive for renderers with internal pointer interactions)
  • Drag-and-drop reorder within column, across columns, and across swimlane cells — drop anywhere in a column, not only onto a card
  • Column reorder by dragging the column header
  • Per-column movement flags (allowReorder, allowIncoming, allowOutgoing, acceptsRendererIds)
  • Per-item lock pins an item against any movement
  • Built-in 6-swatch color palette per column (semantic CSS vars; overridable)
  • Collapsible columns (~40px vertical strip) with auto-expand on drop
  • Optional swimlanes — each (column × lane) cell is its own droppable
  • Soft maxItems cap with overflow chip; no drop blocking
  • Optional CRUD via callbacks (no callback = no affordance); inline editors per renderer
  • Controlled and uncontrolled state
  • Keyboard accessible drag (Space lift, arrows, Space drop, Escape cancel)
  • Read-only mode disables all DnD and CRUD, leaves clicks active
  • Native vertical column scroll when content overflows; vertical mouse-wheel scrolls the board horizontally

Tags

kanbanboarddrag-and-dropdnd-kitswimlanescolumnstaskscard-tree

Dependencies

shadcn primitives: avatar, badge, button, dropdown-menu, input, popover, textarea
npm peer deps: @dnd-kit/core@^6.3.1, @dnd-kit/sortable@^10.0.0, @dnd-kit/utilities@^3.2.2, lucide-react@^1.11.0