Kanban
A board of columns of cards you reorder and move by pointer, touch or keyboard, with announcements and a one-column phone layout.
Board
Drag a card within a column or across columns, or focus it and use Space and the arrow keys. On a phone the board shows one column at a time, the nav scrolls between them and each card has move buttons.
To do
3In progress
2Review
1Done
0todo: t1,t2,t3 | doing: t4,t5 | review: t6 | done:
opened: 0
Code
<Kanban value={state.columns} label="Sprint board">
<KanbanNav>
<KanbanNavItem e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id}>{TITLES[id]}</KanbanNavItem>
</KanbanNav>
<KanbanBoard>
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c}
value={id} title={TITLES[id]} count={state.columns[id].length}>
<KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id} asHandle>
{task.title}
<KanbanItemMove />
</KanbanItem>
</KanbanColumn>
</KanbanBoard>
</Kanban>Usage
import { Kanban, KanbanBoard, KanbanColumn, KanbanItem, KanbanItemMove, KanbanNav, KanbanNavItem } from "@elements/ui/kanban";
<Kanban value={state.columns} label="Sprint board">
<KanbanNav>
<KanbanNavItem e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id}>{TITLES[id]}</KanbanNavItem>
</KanbanNav>
<KanbanBoard>
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c}
value={id} title={TITLES[id]} count={state.columns[id].length}>
<KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id}>
{task.title}
<KanbanItemMove />
</KanbanItem>
</KanbanColumn>
</KanbanBoard>
</Kanban>Examples
Handles and columns
With a handle only the grip drags, so the rest of the card stays clickable and selectable. A column handle reorders columns. A handle can be disabled on its own (the Retry failed webhooks grip and the Done column grip) while its card or column stays enabled. Columns scroll on their own past a height.
Backlog
6In progress
1Done
1backlog: h1,h2,h3,h4,h5,h6 | doing: h7 | done: h8
Code
<Kanban value={state.columns}>
<KanbanBoard>
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
<KanbanColumnHandle slot="actions" disabled={id === "done"} />
<KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id}>
<KanbanItemHandle disabled={task.pinned} />
{task.title}
</KanbanItem>
<Button slot="footer" variant="ghost" size="sm" onclick={() => add(id)}>Add card</Button>
</KanbanColumn>
</KanbanBoard>
</Kanban>Rules
onMove can refuse a move (In progress holds two cards). A disabled card stays put, a disabled column takes no cards and its handle does not drag, and a disabled board does nothing.
To do
3In progress
2Archived
1todo: r1,r2,r3 | doing: r4,r5 | archived: r6
Code
<Kanban value={state.columns} disabled={state.frozen} onMove={(e) => limit(e)}>
<KanbanBoard>
<KanbanColumn value="todo" title="To do">
<KanbanItem value="r2" asHandle disabled>Locked: legal review</KanbanItem>
</KanbanColumn>
<KanbanColumn value="archived" title="Archived" disabled>
<KanbanColumnHandle slot="actions" /> <!-- data-disabled, aria-disabled: it does not drag -->
...
</KanbanColumn>
</KanbanBoard>
</Kanban>
function limit(e: KanbanMoveEvent) {
if (e.to.column === "doing" && state.columns.doing.length >= 2) {
e.preventDefault();
}
}Plain values and a custom overlay
Strings are their own ids. Bind dragging and fill the overlay slot to draw your own drag preview; dragging.kind tells a column drag from a card drag.
Ideas
Now
Shipped
ideas: Search,Exports,Webhooks | now: Billing | shipped: dragging: none moves: 0
Code
<Kanban value={state.columns} dragging={state.dragging}>
<KanbanBoard>
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
<KanbanColumnHandle slot="actions" />
<KanbanItem e:for={name of state.columns[id]} e:key={(n) => n} value={name} asHandle>{name}</KanbanItem>
</KanbanColumn>
</KanbanBoard>
<div slot="overlay" class={["drag-preview", state.dragging?.kind === "column" && "is-column"]} e:if={state.dragging}>
Moving {state.dragging.id}
</div>
</Kanban>Vertical
orientation=vertical stacks the columns. ArrowDown past the last card moves into the next column, and a column handle reorders columns with ArrowUp and ArrowDown.
Now
Next
Later
now: Payments,Refunds | next: Disputes | later: Payouts,Tax
Code
<Kanban value={state.columns} orientation="vertical">
<KanbanBoard>
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
<KanbanColumnHandle slot="actions" />
<KanbanItem e:for={name of state.columns[id]} e:key={(n) => n} value={name} asHandle>{name}</KanbanItem>
</KanbanColumn>
</KanbanBoard>
</Kanban>Restrict to an axis
restrictTo=vertical keeps the dragged card on its column's axis, so a pointer only reorders.
Now
Next
now: One,Two,Three | next: Four
Code
<Kanban value={state.columns} restrictTo="vertical">...</Kanban>Right to left
Columns run from the right; ArrowLeft moves a card to the next column.
To do
In progress
Done
todo: Alpha,Beta | doing: Gamma | done:
Code
<div dir="rtl">
<Kanban value={state.columns}>...</Kanban>
</div>Whole card and column as handle
asHandle on a card makes the whole card drag even with a grip inside. asHandle on a column drags it from its header and background; presses on its cards still move cards.
Column A
Column B
a: 1,2 | b: 3
Code
<Kanban value={state.columns}>
<KanbanBoard>
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]} asHandle>
<KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id} asHandle>
<KanbanItemHandle />
{task.title}
</KanbanItem>
</KanbanColumn>
</KanbanBoard>
</Kanban>Translated, with custom ids
instructions, announcements, roleDescription and the move button labels replace every screen reader text, so all of it can be translated. getItemValue reads an id from any field. attrs sets the board's and each column's id, and class is appended to the KanbanBoard.
Pendiente
Haciendo
Hecho
pendiente: i18n-1,i18n-2 | haciendo: i18n-3 | hecho:
Code
<Kanban value={state.columns}
getItemValue={(t) => t.key}
instructions="Pulsa espacio para mover."
roleDescription="ordenable"
attrs={{ id: "tablero" }}
announcements={{
start: (a) => `Recogido ${a.label}. Posición ${a.position} de ${a.total}.`,
over: (a) => ...,
end: (a) => ...,
cancel: (a) => ...,
}}>
<KanbanBoard class="kanban-demo-board">
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITULOS[id]}
attrs={{ id: `columna-${id}` }}>
<KanbanItem e:for={t of state.columns[id]} e:key={(t) => t.key} value={t.key} asHandle>
{t.titulo}
<KanbanItemMove labels={{
to: "Mover a {column}",
previous: "Mover a la columna anterior",
next: "Mover a la columna siguiente",
noPrevious: "No hay columna anterior",
noNext: "No hay columna siguiente",
}} />
</KanbanItem>
</KanbanColumn>
</KanbanBoard>
</Kanban>Fixed cards and custom keys
A card drags when it has asHandle or a handle; without either it stays where it is and only moves aside for others. keyboardCodes changes the keys that pick up, drop and cancel; pair it with instructions that name them.
This week
Later
week: f1,f2,f3 | later: f4
Code
<Kanban value={state.columns}
keyboardCodes={{ start: ["KeyM"], end: ["KeyM", "Enter"], cancel: ["Escape"] }}
instructions="To pick up a card, press M. Use the arrow keys to move it, M again to drop it, or Escape to cancel.">
<KanbanBoard>
<KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
<!-- Without asHandle or a handle a card does not drag: it only moves aside. -->
<KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id} asHandle={!task.pinned}>
{task.title}
</KanbanItem>
</KanbanColumn>
</KanbanBoard>
</Kanban>Options
flatCursor keeps the default cursor, autoScroll off leaves scrolling to the user, and restrictTo keeps the dragged copy on a row, inside the board or inside the window. KanbanNav always shows at every width. A column with only a label is named without a title.
Now
Next
Someday (label, no title)
now: Alpha,Beta | next: Gamma | later: Delta
Code
<Kanban value={state.columns} flatCursor autoScroll={false} restrictTo="board">
<KanbanNav always>...</KanbanNav>
<KanbanBoard>
<KanbanColumn value="later" label="Someday">
<p slot="header">Someday, no title</p>
...
</KanbanColumn>
</KanbanBoard>
</Kanban>