@lilydesignsystem/vue-kanban-board
| Install | |
|---|---|
npm install @lilydesignsystem/vue-kanban-board |
|
| Latest Version: | 0.8.1 |
| Vue: | 3 |
| License: | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause |
| Last Updated: | Oct 25, 2026 |
| Links: | GitHub · npm |
KanbanBoard (Vue helper)
A reusable Vue 3 headless interactive kanban board. It composes
@lilydesignsystem/vue-headless's KanbanTable family — the same
role="grid" container the headless catalog ships, with zero
behaviour built in — and the same IconButton/Listbox pair every
Vue picker helper already depends on, reused here as a per-card "Move
to…" action menu. Ships no CSS: every visual detail is the consumer's,
via kebab-case class hooks.
Cards move between columns two ways, deliberately never drag-only: pointer drag-and-drop, and a keyboard-accessible move menu. Native drag-and-drop fails WCAG 2.5.7 (Dragging Movements); the research behind this package (see spec/index.md) found that the accessible alternative real products ship is an action menu listing destination columns, not arrow-key dragging.
Usage
<script setup lang="ts">
import { ref } from "vue";
import KanbanBoard from "@lilydesignsystem/vue-kanban-board";
import type { KanbanColumn, KanbanCard } from "@lilydesignsystem/vue-kanban-board";
const columns: KanbanColumn[] = [
{ id: "todo", title: "To Do" },
{ id: "doing", title: "In Progress", wipLimit: 3 },
{ id: "done", title: "Done" },
];
const cards = ref<KanbanCard[]>([
{ id: "1", columnId: "todo", title: "Design the empty state" },
{ id: "2", columnId: "doing", title: "Wire up the API" },
]);
function handleMove(cardId: string, toColumnId: string) {
cards.value = cards.value.map((c) => (c.id === cardId ? { ...c, columnId: toColumnId } : c));
}
</script>
<template>
<KanbanBoard
label="Sprint 12 board"
:columns="columns"
:cards="cards"
@move="handleMove"
:labels="{
cardCount: (count) => `${count} card${count === 1 ? '' : 's'}`,
overLimit: (count, limit) => `Over limit: ${count} of ${limit}`,
moveButton: (card) => `Move ${card.title}`,
moveMenuLabel: 'Move to column',
moveAnnouncement: (title, column) => `${title} moved to ${column}`,
}"
/>
</template>
Props
See spec/index.md §5 for the
full table. Required: label, columns, cards. Every optional
labels.* field gates the control it names — omit a label and that
control simply doesn't render (no baked-in English fallback).
Events
move(cardId, toColumnId)— fires after a card moves, by pointer or by the move menu.
Behaviour
- Rectangular grid: the body has as many rows as the largest column's card count; shorter columns pad with empty cells.
- Card move — pointer: native HTML5 drag-and-drop between columns.
- Card move — keyboard: Enter/Space on a focused card opens a "Move to…" listbox of destination columns — not arrow-key dragging.
- WIP limits:
column.wipLimittriggers a warning state (data-over-limit) once a column's card count exceeds it. - Keyboard: WAI-ARIA APG Grid roving-tabindex, the same model
data-griduses — arrows move within/across columns and clamp, Home/End jump within a column, Ctrl+Home/Ctrl+End jump to the grid's ends. - Announcements: every move announces via one
aria-live="polite"region. - Typed fields and editing (0.6.0):
fields+card.valuesshow typed values under the title;field-editmakes editable types edit in place (click, or F2). See spec §11. - Export (0.6.0):
labels.exportCsv/exportTsv/exportExcelpluslabels.exportHeadersadd CSV, TSV and Excel buttons for the shown (or selected) cards.
Export: copy to clipboard (0.7.0)
- Copy to clipboard:
labels.copynames a button (data-…-export-format="copy"; for the kanban board, Gantt chart and calendar it also needslabels.exportHeaders, like the downloads) that puts exactly what a download would hold — the same cards, scope and order, header row first — on the clipboard as tab-separated text (toClipboardText: no byte-order mark, formula text defused), ready to paste into a spreadsheet.copyToClipboarduses the async Clipboard API and falls back to a hidden, selected<textarea>withexecCommand("copy"); it never throws.labels.copyAnnouncement(rowCount)is announced,labels.copyFailedwhen neither path worked, and thecopyevent andcopyHandler({ text, rowCount })may returnfalseto take over (nothing is copied).
Keyboard: page keys and type-ahead (0.7.0)
- PageUp / PageDown move the cursor ten rows (a
virtualizepage when virtualized), clamping at the ends. - Type-ahead: letters typed in quick succession move to the next card in the focused column whose label starts with them. A lone
xstays the select shortcut whenselectable; it joins a prefix already in progress. A Ctrl, Alt or Meta key, and an unmatched prefix, do nothing.
In-place edit: validation, undo and redo (0.7.0)
- Field
validate(value, values)refuses a value of the right type that is not acceptable (a negative estimate): return the reason. The editor stays open witharia-invalid="true"(cleared as soon as the user types again) andlabels.editRefused(card, header, reason)is announced, falling back tolabels.editInvalid. - With
undoable, Ctrl/Cmd+Z takes back a move first while there is one, then field edits. - Undo and redo: every accepted edit is remembered (the latest 100). Ctrl/Cmd+Z puts the earlier value back and Ctrl/Cmd+Shift+Z (or Ctrl+Y) re-applies one, both through the
field-editevent with the restored value and the one it replaced — the consumer owns the data, so an undo is reported like an edit, and an undo is not itself recorded. A new edit ends the redo chain. Inside a field editor the browser's own undo is left alone; nothing happens in areadonlycomponent or without thefield-editevent.labels.fieldUndoAnnouncement(card, header, text)/labels.fieldRedoAnnouncement(card, header, text)are announced. The stacks are the pureedit-history.ts(recordEdit,undoEdit,redoEdit,historyKey).
Drop cue, labels and direction (0.7.0)
- A live "can't drop here" cue. While a card is dragged, the cell under the pointer carries
data-drop-targetif it would accept the card, anddata-drop-blocked(notdata-drop-target) ifcanMove,allowedColumnsor a full strict WIP limit would refuse it. A refuseddragoveris not cancelled and setsdropEffecttonone, so the browser shows its own not-allowed cursor; an accepted one is cancelled and setsmove. The cues clear on leave, drop and drag end. The reference themes outline an accepted cell with a dashed ring and stripe a refused one. This is the live twin of the announcementlabels.moveBlockedmakes after a refused keyboard move; a server-side refusal (a 422) still snaps the card back throughonMoverejecting. - Every label is overridable. All visible and spoken text is in
labels(emptyColumn,addCard,collapseColumn,laneCount, the announcements and the rest); the only literal in the markup is the decorative+glyph of the add-card button, which has its accessible name fromlabels.addCard. A complete Chinese bundle is tested to leave no English in the DOM. - Right-to-left. The horizontal arrows are visual: in a right-to-left layout ArrowLeft goes to the next column, and Alt+ArrowLeft/Right (move the card) and Alt+Shift+ArrowLeft/Right (move the column) follow the arrow. Vertical keys, Home, End and the page keys are unchanged.
isRtl/logicalKeyare exported. - Tests §8.137–§8.148.
Search (0.7.0)
search (a v-model; the field renders when labels.search names it) narrows the cards: every word must appear, in any order, ignoring case and diacritics. The default text is the card's label, subtitle, badges and every typed field's text. Matches in the title are wrapped in <mark class="kanban-board-match"> (link titles too); labels.searchAnnouncement(matches, total) is announced; Escape in the field clears it. See spec §18.
Non-goals (v1)
Drag-preview/ghost-element rendering, virtualization, undo/redo, column reordering, swimlanes, card selection/bulk-move, search/filter, collapsible columns. See spec/index.md §9 for why each is out.
Lily™ and Lily Design System™ are trademarks.
Related Packages
Lily Design System Vue 3 headless Gantt chart (column-span task bars over GanttT...
Lily Design System Vue 3 headless interactive data grid (sort, filter, selection...
A comprehensive headless Vue 3 component library with 491 accessible, unstyled c...
Lily Design System Vue 3 locale picker: an icon button opening an APG listbox th...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.8.1 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.8.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.7.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.6.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.5.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.4.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.3.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.2.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause | |
| 0.1.0 | ^3.0.0 | MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause |