Self-contained drag-and-drop board. Import `<KanbanBoard />` and pass `columns` + a `cards` record keyed by column id. The same component powers two layouts: a status-grouped **Board view** and a **Manage My Queue** board with a capped Active column. The board manages drag state internally and reports the next mapping via `onChange`.
npx shadcn@latest add https://sdk-components.thesqd.com/r/kanban-board-showcase.jsonimport {
KanbanBoard,
type KanbanBoardProps,
type KanbanColumn,
type KanbanCard,
type KanbanStatus,
type KanbanTone,
} from "@/components/blocks/kanban-board-showcase";"use client";
import * as React from "react";
import {
KanbanBoard,
type KanbanCard,
type KanbanColumn,
} from "@/components/blocks/kanban-board-showcase";
const COLUMNS: KanbanColumn[] = [
{ id: "queued", title: "Queued", tone: "muted", showHandle: true },
{ id: "active", title: "Active", tone: "emerald", cap: 7, disableDnd: true, showPlayPause: true },
];
const INITIAL_CARDS: Record<string, KanbanCard[]> = {
queued: [
{ id: "c1", title: "Stewardship brief", status: { label: "Open", tone: "muted" } },
{ id: "c2", title: "Outreach video", status: { label: "Open", tone: "muted" } },
],
active: [
{ id: "c5", title: "Easter graphics", status: { label: "Active", tone: "brand" } },
{ id: "c7", title: "Annual report cover", status: { label: "Active", tone: "brand" } },
],
};
export function Example() {
const [cards, setCards] = React.useState(INITIAL_CARDS);
return <KanbanBoard columns={COLUMNS} cards={cards} onChange={setCards} />;
}KanbanBoard
├── KanbanColumn[] (props.columns — id, title, tone, cap, showHandle, disableDnd, showPlayPause)
└── Cards record (props.cards — Record<columnId, KanbanCard[]>)
└── KanbanCard
├── title / description
├── status: { label, tone }
├── assignee: { name, initials }
└── paused (only meaningful in showPlayPause columns — flips Pause ↔ Play)
Drag rules
├── Reordering is allowed within a column.
└── Cross-column moves are blocked — cards never leave their source column.KanbanBoard
| Prop | Type | Default | Description |
|---|---|---|---|
columns | KanbanColumn[] | — | Column configuration. |
cards | Record<string, KanbanCard[]> | — | Cards keyed by column id. |
onChange | (next: Record<string, KanbanCard[]>) => void | — | Fired with the new mapping after every drag. |
KanbanColumn
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Matches a key in the cards record. |
title | string | — | Header label. |
tone | "brand" | "emerald" | "amber" | "sky" | "muted" | "fuchsia" | "rose" | — | Color of the header dot. |
cap | number | — | Soft cap; the count pill grades green/yellow/red against it. |
showHandle | boolean | false | Visible drag handle + position marker on each card. |
disableDnd | boolean | false | Lock the column — no drags out, no drops in. |
showPlayPause | boolean | false | Render a small solid Play/Pause toggle on the left of every card. Cards flip between Pause and Play based on `card.paused`. |
KanbanCard
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable card identifier. |
title | string | — | Card title. |
description | string | — | Optional secondary text. |
status | { label: string; tone?: KanbanTone } | — | Status badge. |
assignee | { name: string; initials: string } | — | Avatar slot. |
paused | boolean | false | Only meaningful in `showPlayPause` columns. Controls which icon renders (Play when true, Pause otherwise). Toggled internally on click. |