Data
Kanban
Kanban board with columns of draggable cards — pointer drag & drop, a fully keyboard-accessible move mode with live announcements, WIP-limit marking, locked columns, per-target drop rules and optional column reordering.
Import
import { Kanban } from "@zephora/react";Examples
Basic
Uncontrolled board via `defaultColumns`. `onCardMove` receives the move delta after a card lands somewhere new.
To do3
Design tokens audit
Dark mode QA pass
Docs search index
In progress2
DataTable virtualization
Calendar week view
Done1
Keyboard drag & drop
const columns: KanbanColumnData[] = [
{
id: "todo",
title: "To do",
cards: [
{ id: "k1", title: "Design tokens audit" },
{ id: "k2", title: "Dark mode QA pass" },
{ id: "k3", title: "Docs search index" },
],
},
{
id: "doing",
title: "In progress",
cards: [
{ id: "k4", title: "DataTable virtualization" },
{ id: "k5", title: "Calendar week view" },
],
},
{ id: "done", title: "Done", cards: [{ id: "k6", title: "Keyboard drag & drop" }] },
];
function Board() {
const [lastMove, setLastMove] = React.useState<KanbanCardMoveDetail | null>(null);
return (
<>
<Kanban defaultColumns={columns} onCardMove={setLastMove} />
<Text size="sm" muted>
{lastMove
? `Moved ${lastMove.cardId} from ${lastMove.fromColumnId} to ${lastMove.toColumnId} (position ${lastMove.toIndex + 1}, ${lastMove.method})`
: "Drag a card, or focus one and press Space."}
</Text>
</>
);
}Rules and limits
The locked `Released` column rejects every drop (cards can still be dragged out). `In progress` has a WIP limit of 2 — exceeding it marks the column with `data-over-limit` — and `canDropCard` only lets cards flagged ready enter it.
Backlog3
Fix focus ring
New landing page
Refactor theming
In progress2/2
RTL audit
Perf budget CI
Released1
v2.3.0
<Kanban
defaultColumns={columns}
canDropCard={(card, target) =>
target.id !== "wip" || (card.data as { ready?: boolean })?.ready === true
}
/>Custom cards
`renderCard` replaces the card body; the arbitrary `data` payload travels with the card through every move.
To do2
Ship calendar docs
ALdocs
Sparkline gradients
GHfeature
In review1
Tooltip delay tuning
ATbug
Done1
Chip overflow fix
BLbug
<Kanban
defaultColumns={columns}
renderCard={(card) => {
const meta = card.data as { assignee: string; tag: string };
return (
<div style={{ display: "grid", gap: 8 }}>
<Text size="sm">{card.title}</Text>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Avatar name={meta.assignee} size="xs" />
<Badge variant="soft" color={meta.tag === "bug" ? "danger" : "info"}>
{meta.tag}
</Badge>
</div>
</div>
);
}}
/>API
Kanban props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | KanbanColumnData[] | — | Controlled board state. |
defaultColumns | KanbanColumnData[] | [] | Uncontrolled initial board state. |
onColumnsChange | (columns: KanbanColumnData[]) => void | — | Fires with the full next columns array after any card / column move. |
onCardMove | (detail: KanbanCardMoveDetail) => void | — | Fires with the move delta after a card lands somewhere new. |
onColumnMove | (columnId: string, fromIndex: number, toIndex: number) => void | — | Fires after a column is reordered (requires `reorderableColumns`). |
reorderableColumns | boolean | false | Makes column headers draggable to reorder columns. |
isCardDraggable | (card: KanbanCard, column: KanbanColumnData) => boolean | — | Per-card drag opt-out (disabled cards are never draggable). |
canDropCard | (card: KanbanCard, target: KanbanColumnData) => boolean | — | Per-target drop rule; locked columns are always rejected. |
onCardClick | (card: KanbanCard, column: KanbanColumnData, event: MouseEvent) => void | — | Click handler for enabled cards. |
renderCard | (card, { column, dragging }) => ReactNode | — | Custom card body renderer. |
renderColumnHeader | (column: KanbanColumnData) => ReactNode | — | Custom column header renderer (replaces title + count badge). |
unstyled | boolean | false | Headless mode — skips Zephora styling. |
classNames | Partial<Record<KanbanSlot, string>> | — | Per-slot class overrides, appended after the module classes; also applied in unstyled mode. |
KanbanColumnData props
| Prop | Type | Default | Description |
|---|---|---|---|
id * | string | — | Unique column id. |
title * | ReactNode | — | Column header title. |
cards * | KanbanCard[] | — | Cards in display order. |
locked | boolean | — | Locked columns reject drops (cards can still be dragged out). |
limit | number | — | WIP limit; exceeding it only marks the column with `data-over-limit`. |
KanbanCard props
| Prop | Type | Default | Description |
|---|---|---|---|
id * | string | — | Unique card id. |
title | ReactNode | — | Default card content (falls back to `id`). |
data | unknown | — | Arbitrary consumer payload carried with the card. |
disabled | boolean | — | Disabled cards cannot be dragged, picked up or clicked. |
KanbanCardMoveDetail props
| Prop | Type | Default | Description |
|---|---|---|---|
cardId | string | — | Id of the moved card. |
fromColumnId | string | — | Source column id. |
toColumnId | string | — | Target column id. |
fromIndex | number | — | Index the card left in the source column. |
toIndex | number | — | Final index of the card inside the target column. |
method | "pointer" | "keyboard" | — | Whether the move came from drag & drop or the keyboard move mode. |
Keyboard
| Key | Action |
|---|---|
Tab | Moves focus into / out of the board (one card is tabbable at a time). |
Arrow keys | Move focus between cards — up / down within a column, left / right to the neighbor column. |
Enter / Space | Picks up the focused card (enters move mode). |
Arrow keys (while picked) | Move the pick-up target — up / down within the column, left / right to the nearest column that accepts the card. |
Enter / Space (while picked) | Drops the card at the announced position. |
Escape | Cancels the move and keeps the card in place. |