Kanban board
Cards in columns and lanes, moved by dragging or entirely by keyboard. Moves show at once and save in the background.
Installation
npx shadcn@latest add @quireui-pro/kanban-boardPart of Pro. Add the Pro registry with your token once, as shown in Installation, and the CLI brings in the free parts it builds on as well.
Usage
Hand the board your items and say which field holds a card's column. It reads the column from that field, and a move writes the new column back, so onItemsChange is all it needs to stay in step with your state.
const [issues, setIssues] = useState(initial);
<KanbanBoard
items={issues}
onItemsChange={setIssues}
groupBy="status"
columns={[
{ id: "todo", title: "Todo" },
{ id: "doing", title: "In progress", limit: 3 },
{ id: "done", title: "Done" },
]}
itemKey={(issue) => issue.id}
itemLabel={(issue) => issue.title}
card={(issue) => <IssueCard issue={issue} />}
/>card draws what is inside a card; the frame, focus and menu are the board's. itemLabel is what a card reads as to a screen reader and in messages. Cards are in the order of items.
Give the board a height, with className or its parent, and long columns scroll inside it.
Saving
A move shows at once, and onMove saves it. If it returns a promise that rejects, the card slides back where it was and a toast offers Retry.
<KanbanBoard
// ...
onMove={(issue, move) =>
fetch(`/api/issues/${issue.id}`, {
method: "PATCH",
body: JSON.stringify({ status: move.column, after: move.before?.id }),
}).then((response) => {
if (!response.ok) throw new Error("Not saved");
})
}
/>move holds the column and index, where it came from, and the cards now before and after it, for apps that store an order between neighbours.
Moving cards
With a mouse, a short move picks a card up; with a finger, a short hold, so scrolling a column still scrolls. A slot of its size opens where it will land, the other cards slide aside, and the counts update as it travels, so a limit warns before the drop. Near an edge the board scrolls by itself. Esc puts the card back.
Every card has a menu, with . or the button in its corner, to move it to another column or to the top or bottom of its own. It is the dependable way to move a card on a phone.
Peek
peek shows a card in a panel beside the board, from a click or Enter. It is not modal: the arrow keys keep moving between cards and the panel follows, and Esc closes it. What goes inside is yours. For opening a page instead, use onOpen.
Columns
Past its limit, a column's count turns to a warning; moves are never blocked by it. A column's menu collapses it to a strip that still takes drops, and moves it left or right; dragging its header moves it too. onColumnsChange reports the new order, and storageKey remembers the order and what is collapsed.
With onAdd, the bottom of each column gets a field to type a title into. It stays open for the next one.
Move rules
canDrop says whether a card may go somewhere, like "Done only from In review". While a card is in hand, the places it may not go fade, and holding it over one sends the slot back home, so dropping there changes nothing. The keyboard skips them, and they are off in the card's menu.
<KanbanBoard
// ...
canDrop={(issue, column) => column !== "done" || issue.status === "review"}
/>Lanes
laneBy splits the board into rows by a second field, like the assignee, and lanes lists them from top to bottom. A move across lanes writes the new lane into that field, and move.row tells onMove. The column headers stay on top as the lanes scroll, and a lane collapses from its arrow and still takes drops. For cards with no value, add a lane with id: "".
<KanbanBoard
// ...
laneBy="assignee"
lanes={[
{ id: "ada", title: "Ada Okafor" },
{ id: "", title: "Unassigned" },
]}
/>On a phone
When the board is narrow, in a phone or a side panel, one column fills it and snaps as you swipe, with the edge of the next one showing. A row of pills above shows every column with its count. Dragging a card to the edge moves on to the next column.
Accessibility
Tab stops on the cards once, and the arrow keys do the rest. Every step of a move is announced: "Picked up Rate limit the public API, in Todo, 1 of 3", then "In review, 2 of 3", then where it was dropped. Limits are read out as well as colored.
| Key | Action |
|---|---|
| ↑ / ↓ | Previous or next card, on into the next lane |
| ← / → | The nearest card in the next column |
| Home / End | First or last card in the column |
| Enter | Open the card |
| Space | Pick the card up, then drop it |
| ↑ / ↓ with a card up | Move it within its column |
| ← / → with a card up | Move it to the next column that takes it |
| Shift+↑ / Shift+↓ with a card up | Move it to the next lane |
| Esc | Put the card back, or close the peek |
| . | Open the card's menu |
Motion
Cards slide to their new places in 160ms and a dropped card settles from where you let go; columns do the same. From the keyboard every move is instant, and with reduced motion they jump. The lifted card follows the pointer exactly, without tilt or spring: this is a tool people use all day.
Kanban board or something else?
- Items to sort, filter and compare: the Data table.
- A short list that only changes order: a list with a move menu.
API reference
KanbanBoard
A board of cards in columns, moved by dragging or entirely by keyboard. Moves show at once and save in the background, going back with Retry if saving fails. Columns show their count against a limit, collapse to a strip, and can add cards by typing; laneBy splits the board into rows.
Prop
Type
Style it with [data-slot="kanban-board"].