Press Space or Enter to pick up a card. Use the up and down arrows to move it within a column, left and right to move it between columns, Space or Enter to drop it, and Escape to cancel.
A kanban board: cards dragged within and between columns. The card lifts into a floating ghost that tilts slightly and follows the pointer, a dashed slot shows where it will land while the other cards make room on a spring; on drop the card settles from the ghost into its slot. The board scrolls sideways near its edges and tall columns scroll on their own. On touch a short long press lifts a card, so the board still scrolls under a passing finger. The keyboard does it all too: Space picks a card up, the up and down arrows move it within a column, left and right carry it between columns, Space drops it and Escape puts it back, with every step announced. Columns take a color dot and a work-in-progress limit that tints the header when exceeded. Controlled or uncontrolled, any card shape, your own card content.
The import and the props worth knowing about, in one place.
import { ListKanban, type KanbanColumn } from "@/components/beste/component/list-kanban";
type Task = { id: string; title: string };
const [columns, setColumns] = useState<KanbanColumn<Task>[]>([
{ id: "todo", title: "To do", color: "#a1a1aa", cards: [{ id: "t1", title: "Mix the live album" }] },
{ id: "doing", title: "Doing", color: "#f59e0b", limit: 2, cards: [] },
{ id: "done", title: "Done", color: "#10b981", cards: [] },
]);
<ListKanban
columns={columns}
onChange={(next, move) => {
setColumns(next);
console.log("moved", move.card.title, "to", move.to.column);
}}
getCardLabel={(task) => task.title}
renderCard={(task) => <span className="font-medium">{task.title}</span>}
maxHeight="24rem" // a column's list scrolls past this
tone="outline" // "muted" (default) | "outline" | "ghost"
/>Every one of these is in the component already. They are listed because a props table cannot mention a gesture, so nothing else on this page can tell you they exist.
| Drag | Lift a card into a ghost and carry it within or between columns. The board and tall columns scroll near their edges. |
|---|---|
| Long press (touch) | Lift a card. A finger that moves first scrolls the board instead. |
| Space / Enter | Pick the focused card up, or drop it where it is. |
| Arrow up / down | Move the picked up card within its column. |
| Arrow left / right | Carry it to the neighboring column. |
| Home / End | Move it to the top or the bottom of the column. |
| Escape | Put the card back where it started. Works mid drag too. |
Read from the component's own type, so this cannot drift from what it accepts.
| Prop | Type | Default | Description |
|---|---|---|---|
columns | KanbanColumn<T>[] | — | The columns and their cards. Pair with `onChange` to control them. |
defaultColumns | KanbanColumn<T>[] | — | The starting board when uncontrolled. |
onChange | (columns: KanbanColumn<T>[], move: KanbanMove<T>) => void | — | Called with the new board when a card lands somewhere new. |
getKey | (card: T) => string | — | A stable key per card, unique across the board. Defaults to `card.id`. |
getCardLabel | (card: T) => string | — | What screen readers call a card. Defaults to `label`, `title` or `name`. |
renderCard | (card: T, state: KanbanCardState) => React.ReactNode | — | The card's content. The surface is drawn around it. |
maxHeight | string | "28rem" | Tallest a column's card list grows before it scrolls, any CSS length. |
tone | "muted" | "outline" | "ghost" | "muted" | |
size | "sm" | "default" | "lg" | "default" | |
disabled | boolean | — | |
aria-label | string | "Board" | |
className | string | — | |
columnClassName | string | — | |
cardClassName | string | — |