Component reference
Kanban
Move work through a sprint board with local state.
Preview
Live previewOptional six-digit hex. Clear to restore the preset. Check contrast with your brand color.
Changes apply to this preview only.
No card has moved yet.
Select a card to open its details.
Drag a card to move it, or focus a card and press Control or Alt with the arrow keys. The move menu offers the same actions.
Backlog2 cards
- lowDesign systemIconsMerge the two navigation families and remove the unused glyphs.
- mediumDocsExplain the token rename and the codemod for legacy themes.
To do3 of 3 cards
- urgentAccessibilityRelease blockerTwo amber badges still fail AA on the glass preset.
- highComponentsAuto-resize the textarea and keep Shift + Enter as a newline.
- mediumComponentsA11yControl and Alt with the arrow keys, plus the move menu.
In progress1 of 2 cards
- highPlaygroundCheck light and dark, then the new board and chat pages.
Done2 cards
- mediumTokens
- lowRelease
Width adapts to the available space
"use client";
import { useState } from "react";
import { Kanban, type PimoluKanbanColumn, type PimoluKanbanCard } from "@pimolu/react";
const initialColumns: PimoluKanbanColumn[] = [
{
id: "backlog",
title: "Backlog",
cards: [
{
id: "card-icons",
title: "Consolidate the icon set",
description: "Merge the two navigation families and remove the unused glyphs.",
labels: ["Design system", "Icons"],
assignee: {
id: "maya",
name: "Maya Chen",
},
priority: "low",
},
{
id: "card-docs",
title: "Write the migration guide",
description: "Explain the token rename and the codemod for legacy themes.",
labels: ["Docs"],
assignee: {
id: "noah",
name: "Noah Dubois",
},
priority: "medium",
},
],
},
{
id: "todo",
title: "To do",
limit: 3,
cards: [
{
id: "card-contrast",
title: "Fix the dark-mode contrast audit",
description: "Two amber badges still fail AA on the glass preset.",
labels: [
"Accessibility",
{
label: "Release blocker",
variant: "danger",
},
],
assignee: {
id: "sofia",
name: "Sofia Rossi",
},
priority: "urgent",
},
{
id: "card-chat",
title: "Polish the chat composer",
description: "Auto-resize the textarea and keep Shift + Enter as a newline.",
labels: ["Components"],
assignee: {
id: "lucas",
name: "Lucas Moreau",
},
priority: "high",
},
{
id: "card-kanban",
title: "Add keyboard card moves",
description: "Control and Alt with the arrow keys, plus the move menu.",
labels: ["Components", "A11y"],
assignee: {
id: "maya",
name: "Maya Chen",
},
priority: "medium",
},
],
},
{
id: "doing",
title: "In progress",
limit: 2,
cards: [
{
id: "card-playground",
title: "Review the six presets",
description: "Check light and dark, then the new board and chat pages.",
labels: ["Playground"],
assignee: {
id: "sofia",
name: "Sofia Rossi",
},
priority: "high",
},
],
},
{
id: "done",
title: "Done",
cards: [
{
id: "card-tokens",
title: "Publish the semantic aliases",
labels: ["Tokens"],
assignee: {
id: "noah",
name: "Noah Dubois",
},
priority: "medium",
},
{
id: "card-release",
title: "Cut the 0.9 preview",
labels: ["Release"],
assignee: {
id: "lucas",
name: "Lucas Moreau",
},
priority: "low",
},
],
},
];
export function DemoExample() {
const [columns, setColumns] = useState<PimoluKanbanColumn[]>(initialColumns);
const [lastMove, setLastMove] = useState("No card has moved yet.");
const [selected, setSelected] = useState("Select a card to open its details.");
const [nextCard, setNextCard] = useState(1);
function handleMove(cardId: string, fromColumnId: string, toColumnId: string, index: number) {
const card = columns
.flatMap((column) => column.cards)
.find((entry) => entry.id === cardId);
const from = columns.find((column) => column.id === fromColumnId);
const to = columns.find((column) => column.id === toColumnId);
setLastMove(`${card?.title ?? cardId}: ${from?.title ?? fromColumnId} → ${to?.title ?? toColumnId}, position ${index + 1}.`);
}
function handleCardClick(card: PimoluKanbanCard, columnId: string) {
const column = columns.find((entry) => entry.id === columnId);
setSelected(`${card.title} · ${column?.title ?? columnId}`);
}
function handleAddCard(columnId: string) {
const column = columns.find((entry) => entry.id === columnId);
if(!column ||
(column.limit !== undefined &&
column.cards.length >= column.limit)) {
return;
}
const id = `card-new-${nextCard}`;
setNextCard((value) => value + 1);
setColumns((current) => current.map((entry) => entry.id === columnId
? {
...entry,
cards: [
...entry.cards,
{
id,
title: `New task ${nextCard}`,
description: "Describe the work, then drag the card across the board.",
priority: "medium",
},
],
}
: entry));
}
return (
<div className="grid gap-5 min-w-0">
<div className="flex flex-wrap items-center gap-3 min-w-0 justify-between">
<p className="text-sm text-muted leading-relaxed">{lastMove}</p>
<p className="text-sm text-muted leading-relaxed">{selected}</p>
</div>
<Kanban
columns={columns}
onColumnsChange={setColumns}
onMove={handleMove}
onCardClick={handleCardClick}
onAddCard={handleAddCard}
/>
</div>
);
}More examples
Different configurations and compositions using the real Pimolu API. Each preview runs independently and follows the site appearance.
Inspect and move a task
Move a card by keyboard or menu, then activate it to report the selected task.
Leave a full column
A full column refuses incoming cards, but its existing cards stay enabled. Focus a card and use Control/Alt with an arrow to move it out.
Limits and empty columns
Triage is full at two cards, so it refuses new drops and disables that destination in the move menu. Resolved is empty and still accepts a drop. This board is uncontrolled through defaultColumns.
Usage
Keep column/task ids stable and persist moves in your own data layer.
API reference
Generated from the public TypeScript declarations. Native HTML attributes are inherited where the component extends them; this reference includes Pimolu props and selected common native attributes.
PimoluKanbanAssignee
Type source| Name | Type | Default | Description |
|---|---|---|---|
idRequired | string | — | Public Pimolu prop; see the source declaration for details. |
nameRequired | string | — | Public Pimolu prop; see the source declaration for details. |
avatar | string | undefined | — | Public Pimolu prop; see the source declaration for details. |
idRequiredstringNo default extracted.
Public Pimolu prop; see the source declaration for details.
nameRequiredstringNo default extracted.
Public Pimolu prop; see the source declaration for details.
avatarstring | undefinedNo default extracted.
Public Pimolu prop; see the source declaration for details.
PimoluKanbanCard
Type source| Name | Type | Default | Description |
|---|---|---|---|
idRequired | string | — | Stable card id, used by moves and callbacks. |
titleRequired | string | — | Visible title. |
description | ReactNode | — | Optional supporting text. |
labels | readonly PimoluKanbanLabel[] | undefined | — | Category labels. |
assignee | PimoluKanbanAssignee | undefined | — | Card owner. |
priority | PimoluKanbanPriority | undefined | — | Urgency shown as a Badge. |
idRequiredstringNo default extracted.
Stable card id, used by moves and callbacks.
titleRequiredstringNo default extracted.
Visible title.
descriptionReactNodeNo default extracted.
Optional supporting text.
labelsreadonly PimoluKanbanLabel[] | undefinedNo default extracted.
Category labels.
assigneePimoluKanbanAssignee | undefinedNo default extracted.
Card owner.
priorityPimoluKanbanPriority | undefinedNo default extracted.
Urgency shown as a Badge.
PimoluKanbanColumn
Type source| Name | Type | Default | Description |
|---|---|---|---|
idRequired | string | — | Stable column id. |
titleRequired | string | — | Visible heading. |
limit | number | undefined | — | Maximum number of cards accepted by the column. |
cardsRequired | readonly PimoluKanbanCard[] | — | Ordered cards. Never mutated by the component. |
idRequiredstringNo default extracted.
Stable column id.
titleRequiredstringNo default extracted.
Visible heading.
limitnumber | undefinedNo default extracted.
Maximum number of cards accepted by the column.
cardsRequiredreadonly PimoluKanbanCard[]No default extracted.
Ordered cards. Never mutated by the component.
PimoluKanbanLabels
Type source| Name | Type | Default | Description |
|---|---|---|---|
boardRequired | string | — | Accessible name of the board region. |
instructionsRequired | string | — | Keyboard instructions exposed through aria-describedby. |
addCardRequired | string | — | Visible text of the add-card button. |
addCardToRequired | (columnTitle: string) => string | — | Accessible name of the add-card button in a given column. |
emptyColumnRequired | string | — | Shown inside a column without cards. |
cardRequired | (cardTitle: string, columnTitle: string) => string | — | Accessible name of a card. |
countRequired | (count: number, limit?: number) => string | — | Visible card counter, for example "3 of 5 cards". |
fullRequired | string | — | Badge shown when a column reached its limit. |
moveRequired | (cardTitle: string) => string | — | Accessible name of the move menu trigger. |
moveUpRequired | string | — | Move menu entry. |
moveDownRequired | string | — | Move menu entry. |
moveToRequired | (columnTitle: string) => string | — | Move menu entry pointing at another column. |
movedRequired | (cardTitle: string, columnTitle: string, position: number) => string | — | Live announcement after a successful move. |
boardRequiredstringNo default extracted.
Accessible name of the board region.
instructionsRequiredstringNo default extracted.
Keyboard instructions exposed through aria-describedby.
addCardRequiredstringNo default extracted.
Visible text of the add-card button.
addCardToRequired(columnTitle: string) => stringNo default extracted.
Accessible name of the add-card button in a given column.
emptyColumnRequiredstringNo default extracted.
Shown inside a column without cards.
cardRequired(cardTitle: string, columnTitle: string) => stringNo default extracted.
Accessible name of a card.
countRequired(count: number, limit?: number) => stringNo default extracted.
Visible card counter, for example "3 of 5 cards".
fullRequiredstringNo default extracted.
Badge shown when a column reached its limit.
moveRequired(cardTitle: string) => stringNo default extracted.
Accessible name of the move menu trigger.
moveUpRequiredstringNo default extracted.
Move menu entry.
moveDownRequiredstringNo default extracted.
Move menu entry.
moveToRequired(columnTitle: string) => stringNo default extracted.
Move menu entry pointing at another column.
movedRequired(cardTitle: string, columnTitle: string, position: number) => stringNo default extracted.
Live announcement after a successful move.
PimoluKanbanProps
Type source| Name | Type | Default | Description |
|---|---|---|---|
columns | readonly PimoluKanbanColumn[] | undefined | — | Controlled columns. When provided, the component renders this snapshot verbatim; update it from `onMove` or `onColumnsChange`. |
defaultColumns | readonly PimoluKanbanColumn[] | undefined | — | Initial columns when the board is uncontrolled. |
onColumnsChange | ((columns: PimoluKanbanColumn[]) => void) | undefined | — | Called with the next columns after any valid move. |
onMove | ((cardId: string, fromColumnId: string, toColumnId: string, index: number) => void) | undefined | — | Called after a valid drag, drop, keyboard or menu move. `index` is the zero-based position of the card in the destination column, once the move is applied. |
onCardClick | ((card: PimoluKanbanCard, columnId: string) => void) | undefined | — | Called when a card body/title is activated. |
onAddCard | ((columnId: string) => void) | undefined | — | Adds an "Add card" button to every column when provided. |
labels | Partial<PimoluKanbanLabels> | undefined | — | Overrides the built-in accessible labels. |
onChange | ChangeEventHandler<HTMLDivElement, Element> | undefined | — | Standard React/HTML attribute. |
defaultValue | string | number | readonly string[] | undefined | — | Standard React/HTML attribute. |
className | string | undefined | — | Standard React/HTML attribute. |
style | CSSProperties | undefined | — | Standard React/HTML attribute. |
aria-label | string | undefined | — | Defines a string value that labels the current element. |
onClick | MouseEventHandler<HTMLDivElement> | undefined | — | Standard React/HTML attribute. |
columnsreadonly PimoluKanbanColumn[] | undefinedNo default extracted.
Controlled columns. When provided, the component renders this snapshot verbatim; update it from `onMove` or `onColumnsChange`.
defaultColumnsreadonly PimoluKanbanColumn[] | undefinedNo default extracted.
Initial columns when the board is uncontrolled.
onColumnsChange((columns: PimoluKanbanColumn[]) => void) | undefinedNo default extracted.
Called with the next columns after any valid move.
onMove((cardId: string, fromColumnId: string, toColumnId: string, index: number) => void) | undefinedNo default extracted.
Called after a valid drag, drop, keyboard or menu move. `index` is the zero-based position of the card in the destination column, once the move is applied.
onCardClick((card: PimoluKanbanCard, columnId: string) => void) | undefinedNo default extracted.
Called when a card body/title is activated.
onAddCard((columnId: string) => void) | undefinedNo default extracted.
Adds an "Add card" button to every column when provided.
labelsPartial<PimoluKanbanLabels> | undefinedNo default extracted.
Overrides the built-in accessible labels.
onChangeChangeEventHandler<HTMLDivElement, Element> | undefinedNo default extracted.
Standard React/HTML attribute.
defaultValuestring | number | readonly string[] | undefinedNo default extracted.
Standard React/HTML attribute.
classNamestring | undefinedNo default extracted.
Standard React/HTML attribute.
styleCSSProperties | undefinedNo default extracted.
Standard React/HTML attribute.
aria-labelstring | undefinedNo default extracted.
Defines a string value that labels the current element.
onClickMouseEventHandler<HTMLDivElement> | undefinedNo default extracted.
Standard React/HTML attribute.
Accessibility
- A full column blocks incoming moves and additions. Existing cards remain enabled so users can move them out.
- Provide a keyboard move path and announce the resulting destination; do not require drag and drop.