Kanban
Accessible, unstyled Kanban board: columns of cards with roving keyboard navigation and pointer + keyboard drag and drop (built on the shared drag-and-drop foundation), WIP limits, veto hooks, and announcements.
| Package | @vizejs/ui/kanban |
| Maturity | stable |
| Own the source | vize lib pull kanban |
| Requires | context, drag-and-drop, id |
| Aliases | task board, kanban board, swimlanes, card board |
| Covers | WAI-ARIA drag-and-drop guidance, dnd-kit sortable board, Atlassian board |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { Kanban, KanbanCard, KanbanColumn } from "@vizejs/ui/kanban";
Or copy the source into your project with vize lib pull kanban (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Sprint board bound with v-model: cards move between columns by pointer or keyboard, with a WIP limit. -->
<script setup lang="ts">
import { ref } from "vue";
import { Kanban, type KanbanBoard, type KanbanColumnDefinition } from "@vizejs/ui/kanban";
type Lane = "todo" | "doing" | "done";
interface Task {
readonly id: string;
readonly title: string;
}
const columns: readonly KanbanColumnDefinition<Lane>[] = [
{ id: "todo", title: "To do" },
{ id: "doing", title: "In progress", limit: 2 },
{ id: "done", title: "Done" },
];
const board = ref<KanbanBoard<Task, Lane>>({
todo: [
{ id: "t1", title: "Write release notes" },
{ id: "t2", title: "Audit color contrast" },
],
doing: [{ id: "t3", title: "Fix date picker focus" }],
done: [{ id: "t4", title: "Upgrade to Vue 3.5" }],
});
</script>
<template>
<Kanban
v-model="board"
aria-label="Sprint board"
:columns
:get-card-key="(task: Task) => task.id"
:get-card-label="(task: Task) => task.title"
>
<template #column="{ column, cards, full }">
{{ column.title }} ({{ cards.length
}}{{ column.limit === undefined ? "" : ` of ${column.limit}` }})
<template v-if="full"> · full</template>
</template>
<template #card="{ card }">{{ card.title }}</template>
<template #empty>No tasks</template>
</Kanban>
</template>
API
Kanban<Card, ColumnId extends string>
Source: src/families/data/kanban/kanban.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns (required) |
readonly KanbanColumn<ColumnId>[] |
undefined |
Board columns in display order. |
modelValue (required) |
KanbanBoard<Card, ColumnId> |
undefined |
Cards per column (v-model). |
getCardKey (required) |
(card: Card) => string |
undefined |
Stable key per card. |
getCardLabel |
(card: Card) => string |
undefined |
Card name for announcements; defaults to the rendered text. |
canMove |
(card: Card, from: ColumnId, to: ColumnId) => boolean |
undefined |
Veto a move before it is offered as a drop target. |
id |
string | null |
undefined |
Consumer-owned board id. null and undefined select a deterministic fallback. |
dir |
"ltr" | "rtl" |
"ltr" |
Reading direction for Left/Right column navigation. |
instructions |
string |
"Press Space or Enter to pick up the card. …" |
Keyboard instructions referenced by every card's aria-describedby. |
announcements |
KanbanAnnouncements |
undefined |
Localized grab, move, drop, and cancel announcements. |
ariaLabel |
string |
undefined |
Accessible name of the board. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: KanbanBoard<Card, ColumnId>] |
Fired with the board after a move (supports v-model). |
move |
[event: KanbanMoveEvent<Card, ColumnId>] |
Fired after a move commits with origin and destination. |
Slots
| Slot | Slot props | Description |
|---|---|---|
column |
KanbanColumnSlotProps<Card, ColumnId> |
Column header contents. Defaults to the column title. |
card |
KanbanCardSlotProps<Card, ColumnId> |
Card contents. Defaults to the card label or key. |
empty |
{ readonly column: KanbanColumn<ColumnId> } |
Rendered inside columns without cards. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLDivElement | null |
Rendered board element. |
focusCard |
(cardKey?: string) => void |
Focus a card by key (or the current tab stop). |
cancelDrag |
() => boolean |
Cancel an active drag. |
KanbanCard
Source: src/families/data/kanban/kanban-card.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
cardKey (required) |
string |
undefined |
Stable card key. |
columnId (required) |
string |
undefined |
Column the card is in. |
index (required) |
number |
undefined |
Zero-based index, published as aria-posinset (one-based). |
label |
string |
undefined |
Name used by drag announcements; defaults to the rendered text. |
disabled |
boolean |
false |
Keep the card focusable but not draggable. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly dragging: boolean } |
Card contents. Receives the drag state. |
KanbanColumn
Source: src/families/data/kanban/kanban-column.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columnId (required) |
string |
undefined |
Column id. |
title (required) |
string |
undefined |
Column title and accessible name. |
count |
number |
0 |
Number of cards, published as aria-setsize context and data-count. |
limit |
number |
undefined |
WIP limit, published as data-limit. |
disabled |
boolean |
false |
Refuse drops. |
Slots
| Slot | Slot props | Description |
|---|---|---|
header |
— | Column header contents. |
default |
— | Cards (KanbanCard elements). |
Behavior
@vizejs/ui/kanban renders a board of labelled columns (kanban-column.vue) holding focusable cards
(kanban-card.vue); kanban.vue is the generic root (Card, ColumnId). Pointer and keyboard moves use
the shared drag-and-drop foundation (cards are sources and "before" targets; column lists are "end"
targets). Every row is proven by the named test in kanban.test.ts or kanban-ssr.test.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| K1 | any | render | labelled board, role="group" columns named by their header, lists of listitem cards described by instructions |
renders labelled column groups with lists of focusable cards and instructions |
| K2 | card focus | arrows, Home/End | roving focus moves within a column and across columns (index clamped, empty columns skipped) | arrow keys move the roving focus within and across columns |
| K3 | card focus | Space, arrows, Enter | keyboard drag cycles valid targets, drops into another column, emits move + update:modelValue, keeps focus |
keyboard drag moves a card into another column and keeps focus on it |
| K4 | keyboard drag | Escape | cancels without moving | Escape cancels a keyboard drag without moving |
| K5 | limits and vetoes | keyboard drag | full (limit) and disabled columns and canMove vetoes are never offered as targets |
WIP limits, disabled columns, and canMove veto drop targets |
| K6 | pointer drag | release over a card's lower half | the drop edge is published and the card lands after it | dropping on a card's bottom edge inserts after it |
| K7 | custom boards | import | KanbanColumn and KanbanCard parts are exported |
column and card parts are exported for custom boards |
| K8 | SSR | two requests + hydrate | byte-identical markup with one tab stop and no drag state; warning-free hydration | renders identical board markup across SSR requests and hydrates cleanly |