Kanban Board
import { Kanban, type KanbanCard, type KanbanColumn } from '@absoluteui/components/kanban';import '@absoluteui/components/kanban/style.css';Using Angular? See Kanban Board for Angular.
What it does
Section titled “What it does”A board of columns (workflow states) and optional swimlanes (rows) holding cards. Columns show card counts and work-in-progress (WIP) limits, collapse to a narrow strip, and reorder. Cards move by pointer drag, by keyboard (Space picks a card up, arrow keys move it between positions, columns, and swimlanes, Space drops, Escape cancels), or with a single pointer (toolbar Move card, then a click on the target card or cell).
Every move is dispatched first as the cancelable aui-card-move, applied immediately, then saved through the optional moveCard callback (with an AbortSignal and a timeout). A rejected or timed-out save moves the card back, announces the failure, and emits aui-move-error. Moves and column reorders are undoable. Cells with many cards render only the cards in view. The model helpers (moveKanbanCard, positionOf, cellCards, columnCounts) are pure.
When to use
Section titled “When to use”- Tracking work through workflow states (to do, in progress, done) with limits on work in progress.
- Splitting a board by team, priority, or product with swimlanes.
- Letting users re-prioritize and move items with the pointer or the keyboard, saved to a server.
When not to use
Section titled “When not to use”- Tabular data with many attributes per item: use Data Table.
- Items that must be scheduled on dates: use Scheduler or Gantt.
- Reordering a single list: use Sortable List.
Examples
Section titled “Examples”A board with WIP limits
Section titled “A board with WIP limits”import { Kanban, type KanbanCard, type KanbanCardActivateDetail, type KanbanColumn } from '@absoluteui/components/kanban';
const columns: KanbanColumn[] = [ { id: 'todo', label: 'To do' }, { id: 'doing', label: 'In progress', wipLimit: 3 }, { id: 'done', label: 'Done' },];const cards: KanbanCard[] = [ { id: 'a', columnId: 'todo', title: 'Write the spec', tags: ['docs'] }, { id: 'b', columnId: 'doing', title: 'Fix login', description: 'Safari only', color: 3 },];
const host = document.createElement('div');document.body.append(host);Kanban.create(host, { label: 'Sprint 12', columns, cards });host.addEventListener('aui-card-activate', (event) => { host.dataset.open = (event as CustomEvent<KanbanCardActivateDetail>).detail.card.title;});Save moves on a server (rollback on failure)
Section titled “Save moves on a server (rollback on failure)”import { Kanban, type KanbanCard, type KanbanMoveErrorDetail } from '@absoluteui/components/kanban';
const host = document.querySelector<HTMLElement>('#board');if (host) { const cards: KanbanCard[] = [{ id: '42', columnId: 'new', title: 'Refund request' }]; Kanban.create(host, { label: 'Support tickets', columns: [{ id: 'new', label: 'New' }, { id: 'open', label: 'Open', wipLimit: 10 }, { id: 'closed', label: 'Closed' }], cards, wipPolicy: 'block', requestTimeoutMs: 10_000, moveCard: async (move, signal) => { const response = await fetch(`/api/tickets/${encodeURIComponent(move.id)}/position`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ column: move.to.columnId, index: move.to.index }), signal, }); if (!response.ok) throw new Error(`Move failed: ${String(response.status)}`); }, }); host.addEventListener('aui-move-error', (event) => { host.dataset.failed = (event as CustomEvent<KanbanMoveErrorDetail>).detail.id; });}Swimlanes, locked columns, and a move veto
Section titled “Swimlanes, locked columns, and a move veto”import { Kanban, type KanbanCardMoveDetail } from '@absoluteui/components/kanban';
const host = document.createElement('div');document.body.append(host);const board = Kanban.create(host, { label: 'Releases', columns: [{ id: 'dev', label: 'Development' }, { id: 'qa', label: 'QA' }, { id: 'live', label: 'Live', locked: true }], lanes: [{ id: 'web', label: 'Web' }, { id: 'app', label: 'Mobile app' }], cards: [ { id: 'w1', columnId: 'dev', laneId: 'web', title: 'New header' }, { id: 'm1', columnId: 'qa', laneId: 'app', title: 'Push notifications' }, ], // Cards stay in their swimlane. canMove: (card, to) => to.laneId === card.laneId,});host.addEventListener('aui-card-move', (event) => { const { to } = (event as CustomEvent<KanbanCardMoveDetail>).detail; if (to.columnId === 'qa' && !window.confirm('Send to QA?')) event.preventDefault();});board.setCollapsed('lane', 'app', true);Custom card content
Section titled “Custom card content”import { Kanban } from '@absoluteui/components/kanban';
const host = document.createElement('div');document.body.append(host);Kanban.create(host, { label: 'Hiring', columns: [{ id: 'applied', label: 'Applied' }, { id: 'interview', label: 'Interview' }], cards: [{ id: 'c1', columnId: 'applied', title: 'Jordan Blake', description: 'Frontend engineer' }], // Non-interactive content only: the card itself is the focusable item. renderCard: (card, { column, doc }) => { const name = doc.createElement('strong'); name.textContent = card.title; const meta = doc.createElement('span'); meta.textContent = `, ${card.description ?? ''} (${column.label})`; const box = doc.createElement('span'); box.append(name, meta); return box; },});Pure model helpers (no DOM)
Section titled “Pure model helpers (no DOM)”import { columnCounts, moveKanbanCard, positionOf, type KanbanCard } from '@absoluteui/components/kanban';
const cards: KanbanCard[] = [ { id: 'a', columnId: 'todo', title: 'A' }, { id: 'b', columnId: 'todo', title: 'B' },];const next = moveKanbanCard(cards, 'b', { columnId: 'done', laneId: null, index: 0 });const where = positionOf(next, 'b', false);document.body.dataset.done = String(columnCounts(next).get('done') ?? 0) + (where ? ` at ${String(where.index)}` : '');Block full columns everywhere
Section titled “Block full columns everywhere”import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { kanban: { wipPolicy: 'block', headingLevel: 2, virtualThreshold: 200 } } });import { Kanban, type KanbanCard, type KanbanCardMoveDetail } from '@absoluteui/components/kanban';import '@absoluteui/components/kanban/style.css';
let saved: KanbanCard[] = [ { id: 'a', columnId: 'todo', title: 'Write the spec', tags: ['docs'] }, { id: 'b', columnId: 'doing', title: 'Fix login', color: 2 },];const host = document.querySelector<HTMLElement>('#board');if (host) { Kanban.create(host, { label: 'Sprint 12', columns: [{ id: 'todo', label: 'To do' }, { id: 'doing', label: 'In progress', wipLimit: 3 }, { id: 'done', label: 'Done' }], cards: saved, }); host.addEventListener('aui-card-move', (event) => { saved = (event as CustomEvent<KanbanCardMoveDetail>).detail.cards; });}More usage
Section titled “More usage”See section 33 and metadata/kanban/examples.json: a basic board with limits, server saves with rollback, swimlanes with vetoes, custom cards, the pure model, global configuration.
Anatomy
Section titled “Anatomy”<div class="aui-kanban [--editable] [--lanes] [--picking]"> (consumer host) <div class="aui-kanban__toolbar" role="group" aria-label="{label} controls"> <button class="aui-kanban__tool" data-aui-action="move|undo|redo" [aria-pressed] [aria-disabled]> <div class="aui-kanban__board" role="group" aria-label="{label}"> (scrolls both ways) <div class="aui-kanban__row aui-kanban__columns"> (sticky header row) <div class="aui-kanban__column-header [--collapsed] [--over] [--full] [--held]" data-aui-column="todo"> <button class="aui-kanban__column-handle" aria-label="Move column To do" aria-pressed="false"> <span class="aui-kanban__column-title" role="heading" aria-level="3"> <span class="aui-kanban__column-name">To do</span> <span class="aui-kanban__count">2 of 3</span> <span class="aui-kanban__over">Over limit</span> <button class="aui-kanban__column-toggle" aria-label="Cards in To do" aria-expanded="true"> <div class="aui-kanban__lanes"> <section class="aui-kanban__lane [--collapsed]" data-aui-lane="web" aria-labelledby="..."> (a <div> without swimlanes) <div class="aui-kanban__lane-header" role="heading" aria-level="3"><button class="aui-kanban__lane-toggle" aria-expanded="true"> <div class="aui-kanban__row aui-kanban__cells"> <div class="aui-kanban__cell [--collapsed] [--drop] [--no-drop]" data-aui-column="todo" data-aui-lane="web"> <ul class="aui-kanban__list [--virtual]" role="list" aria-label="To do, Web"> <li class="aui-kanban__card aui-kanban__card--c3 [--held] [--picked] [--locked]" data-aui-card="a" tabindex="0" aria-posinset="1" aria-setsize="2" aria-describedby="{instructions}" [aria-busy]> <span class="aui-kanban__grip" aria-hidden="true"> <div class="aui-kanban__card-body"> title, description, tags </div> <span class="aui-kanban__state">, Saving</span> <div class="aui-kanban__empty">No cards</div> <div class="aui-kanban__summary">3 cards</div> <p class="aui-kanban__instructions" id="...">...</p> (visually hidden)API reference
Section titled “API reference”Factory
Section titled “Factory”Kanban.create(host: HTMLElement, options: KanbanOptions): Kanban; moveKanbanCard(cards, id, to: KanbanTarget): KanbanCard[]; positionOf(cards, id, lanes: boolean): KanbanPosition | null; cellCards(cards, columnId, laneId): KanbanCard[]; columnCounts(cards): Map<string, number>Global configuration key: kanban (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
label |
string |
Accessible name of the board (role=group); the toolbar is named ‘{label} controls’. Non-empty. Required. | |
columns |
readonly KanbanColumn[] |
Columns in display order: { id (unique), label, wipLimit? (1..10000 cards across swimlanes), collapsed? (initial), locked? (no cards move into or out of it) }. At least one. Replacing it clears the undo history. Required. | |
lanes |
readonly KanbanLane[] |
Swimlanes (rows): { id, label, collapsed? }. With lanes every card needs a laneId. | |
cards |
readonly KanbanCard[] |
Cards: { id (unique), columnId, laneId? (required with lanes), title, description?, tags? (chips), color? (1..8 accent stripe), locked? }. The order within a cell is the array order. Validated (KANBAN_OPTIONS). Replacing it clears the undo history and aborts pending saves. Required. | |
editable |
boolean |
true |
Cards (and columns) can be moved. false hides the toolbar, grips, and column handles; arrow-key navigation and Enter still work. |
columnReorder |
boolean |
true |
Show column handles for reordering columns (when editable). |
renderCard |
(card: Readonly<KanbanCard>, context: KanbanRenderContext) => Node |
Card content ({ column, lane, doc }). Return non-interactive content: the card is the focusable list item and its text is read by screen readers. Default: title, description, tags. | |
canMove |
(card: Readonly<KanbanCard>, to: Readonly<KanbanPosition>) => boolean |
Veto a move (keyboard moves skip refused columns; pointer drops show an invalid target). Locked cards and columns, and full columns under wipPolicy ‘block’, are always refused. | |
moveCard |
(move: KanbanCardMoveDetail, signal: AbortSignal) => Promise<void> | void |
Save a move after it was applied. While it runs the card is aria-busy and cannot move again. A rejection or a timeout (requestTimeoutMs) moves the card back, removes the move from the history, announces the failure, and emits aui-move-error. | |
wipPolicy |
'warn' | 'block' |
"warn" |
‘warn’ marks columns over their limit (‘Over limit’ text and icon); ‘block’ also refuses moves into a column at its limit (moves within the column stay possible). Globally configurable. |
virtualThreshold |
number |
150 |
Cells with more cards than this (20..100000) scroll on their own and render only the cards in view (measured heights, spacers, aria-setsize/aria-posinset). Globally configurable. |
requestTimeoutMs |
number |
30000 |
moveCard timeout in milliseconds (100..600000). Globally configurable. |
historyLimit |
number |
50 |
Undo depth (0..500); 0 disables undo. Globally configurable. |
headingLevel |
number |
3 |
aria-level of column and swimlane headings (2..6). Globally configurable. |
locale |
string |
"en-US" |
Numbers and messages. Globally configurable. Inherited from common configuration. |
direction |
'ltr' | 'rtl' |
"ltr" |
Swaps ArrowLeft/ArrowRight between columns. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
Consumer host. |
element |
HTMLDivElement |
The scrolling board (role=group). |
cards |
KanbanCard[] |
Every card in board order (copies). |
columns |
KanbanColumn[] |
Columns in the current order; collapsed reflects the board. |
editable |
boolean |
Whether editing is on. |
canUndo |
boolean |
A move or column reorder can be undone. |
canRedo |
boolean |
An undone change can be redone. |
pending |
string[] |
Ids of cards whose move is being saved. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
update(options: Partial<KanbanOptions>): void |
Merge and apply options; new cards or columns clear the history. |
positionOf(id: string): KanbanPosition | null |
Column, swimlane (null without swimlanes), and index of a card. |
moveCard(id: string, to: KanbanTarget): boolean |
Move a card (index defaults to the end of the cell). Goes through the same checks, event, save, and history as a user move; false when refused or cancelled. |
moveColumn(id: string, index: number): boolean |
Move a column in the column order (undoable, emits aui-column-move). |
setCollapsed(kind: 'column' | 'lane', id: string, collapsed: boolean): void |
Collapse or expand a column or swimlane (emits aui-collapse-change). |
focusCard(id: string): void |
Focus a card, expanding its column and swimlane and scrolling a virtual cell to it. |
undo(): boolean |
Undo the last move or column reorder (saved through moveCard with reason ‘undo’). Refused while saves are pending. |
redo(): boolean |
Redo the last undone change. |
destroy(): void |
Abort pending saves, remove listeners and the rendered board. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-card-activate |
{ id: string; card: KanbanCard } |
Yes | No | Enter on a card, or a click (outside a single-pointer move). |
aui-card-move |
{ reason: 'move' | 'undo' | 'redo'; id: string; card: KanbanCard; from: KanbanPosition; to: KanbanPosition; cards: KanbanCard[] } |
Yes | Yes | Before a move is applied; preventDefault() keeps the card where it was. |
aui-move-error |
{ id: string; reason: KanbanMoveReason; error: unknown } |
Yes | No | moveCard rejected or timed out; the card is back where it was. |
aui-column-move |
{ reason: KanbanMoveReason; id: string; from: number; to: number; order: string[] } |
Yes | Yes | Before a column reorder is applied; preventDefault() keeps the order. |
aui-collapse-change |
{ kind: 'column' | 'lane'; id: string; collapsed: boolean } |
Yes | No | A column or swimlane was collapsed or expanded. |
Content areas
Section titled “Content areas”| Area | Description |
|---|---|
renderCard |
Custom, non-interactive card content. |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-kanban | --editable | --lanes | --picking |
Host and modifiers; –aui-kanban-column-width, –aui-kanban-collapsed-width, –aui-kanban-list-height size the board. |
aui-kanban__toolbar | __tool | __tool-text |
Toolbar (Move, Undo, Redo). |
aui-kanban__board | __row | __columns | __column-header (--collapsed, --held, --over, --full) | __column-handle | __column-title | __column-name | __count | __over | __column-toggle |
Board and column headers. |
aui-kanban__lanes | __lane (--collapsed) | __lane-header | __lane-toggle | __lane-name | __cells |
Swimlanes. |
aui-kanban__cell (--collapsed, --locked, --drop, --no-drop) | __list (--virtual) | __spacer | __empty | __summary |
Cells and card lists. |
aui-kanban__card (--c1..--c8, --accent, --held, --dragging, --picked, --locked) | __grip | __card-body | __title | __description | __tags | __tag | __state |
Cards (the drag offset is –aui-kanban-drag-x / –aui-kanban-drag-y). |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
move / grip / column handle |
grip |
undo |
undo |
redo |
redo |
over limit |
alert-triangle |
collapse |
chevron-down / chevron-right |
Design tokens
Section titled “Design tokens”Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.
--aui-border-width | --aui-border-width-strong | --aui-color-bg | --aui-color-border | --aui-color-border-strong | --aui-color-chart-1 | --aui-color-chart-2 | --aui-color-chart-3 | --aui-color-chart-4 | --aui-color-chart-5 | --aui-color-chart-6 | --aui-color-chart-7 | --aui-color-chart-8 | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-color-warning-fg | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-sm | --aui-font-size-xs | --aui-font-weight-medium | --aui-font-weight-regular | --aui-font-weight-semibold | --aui-icon-size-sm | --aui-line-height-tight | --aui-radius-md | --aui-radius-pill | --aui-shadow-lg | --aui-shadow-sm | --aui-space-1 | --aui-space-2 | --aui-touch-target-min
TypeScript definitions
Section titled “TypeScript definitions”interface KanbanColumn { id: string; label: string; wipLimit?: number; collapsed?: boolean; locked?: boolean }interface KanbanLane { id: string; label: string; collapsed?: boolean }type KanbanCardColor = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;interface KanbanCard { id: string; columnId: string; laneId?: string; title: string; description?: string; tags?: readonly string[]; color?: KanbanCardColor; locked?: boolean }interface KanbanPosition { columnId: string; laneId: string | null; index: number }type KanbanTarget = Omit<KanbanPosition, 'index'> & { index?: number };type KanbanWipPolicy = 'warn' | 'block';type KanbanMoveReason = 'move' | 'undo' | 'redo';interface KanbanRenderContext { column: KanbanColumn; lane: KanbanLane | null; doc: Document }interface KanbanCardMoveDetail { reason: KanbanMoveReason; id: string; card: KanbanCard; from: KanbanPosition; to: KanbanPosition; cards: KanbanCard[] }interface KanbanCardActivateDetail { id: string; card: KanbanCard }interface KanbanMoveErrorDetail { id: string; reason: KanbanMoveReason; error: unknown }interface KanbanColumnMoveDetail { reason: KanbanMoveReason; id: string; from: number; to: number; order: string[] }interface KanbanCollapseChangeDetail { kind: 'column' | 'lane'; id: string; collapsed: boolean }Accessibility
Section titled “Accessibility”Pattern: Headed lists: a named group (the board) containing column headings and, with swimlanes, labelled sections; each cell is a named list (role=list) of focusable list items (cards) with aria-posinset and aria-setsize. One roving tab stop covers all cards; arrow keys move between cards and columns. Moves follow the Sortable List grab-and-move model (Space picks up, arrows move, Space drops, Escape cancels) with live-region announcements, plus a single-pointer path through the toolbar (WCAG 2.5.7). No deprecated ARIA drag-and-drop attributes.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Tab / Shift+Tab | Board | Toolbar buttons, column handles and toggles, swimlane toggles, then one tab stop for the cards. |
| ArrowDown / ArrowUp | Card focused | Next / previous card in the cell; past the end, the first (last) card of the same column in the next (previous) swimlane. |
| ArrowRight / ArrowLeft (swapped in RTL) | Card focused | The card at the same position (or the last one) in the next non-empty expanded column of the swimlane. |
| Home / End | Card focused | First / last card of the cell; with Ctrl, of the board. |
| Enter | Card focused | Activate (aui-card-activate). |
| Space | Card focused (editable) | Pick the card up (refused and announced for locked cards and cards being saved). |
| ArrowUp / ArrowDown | Card picked up | Move one position; past the edges into the previous / next swimlane. |
| ArrowLeft / ArrowRight | Card picked up | Move to the adjacent expanded column that accepts it (full columns under ‘block’, locked columns, and canMove vetoes are skipped). |
| Home / End | Card picked up | Move to the first / last position of the cell. |
| Space / Enter | Card picked up | Drop: dispatch aui-card-move, apply, save. |
| Escape / Tab | Card picked up | Cancel; the card returns (Escape does not close an enclosing overlay). |
| Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y | Card focused (editable) | Undo / redo. |
| Space or Enter, ArrowLeft / ArrowRight, Home / End, Space or Enter, Escape | Column handle | Pick the column up, move it, drop it, or cancel. |
| Escape | Single-pointer move pending | Cancel the pending move. |
| Element | Role |
|---|---|
| toolbar <div> | group, named ‘{label} controls’ |
| board <div> | group, named by aria-label |
| column title | heading (aria-level = headingLevel), text: name and count |
| column handle, column toggle, lane toggle, toolbar buttons | button (native) |
| swimlane <section> | region (named by its heading) |
| swimlane header | heading containing the toggle button |
| cell <ul> | list, named ‘{column}’ or ‘{column}, {lane}’ |
| card <li> | listitem, focusable, described by the instructions |
| grip, spacer <li> | none (aria-hidden=true) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-label |
board / toolbar / cell list | label / ‘{label} controls’ / column and swimlane names |
aria-level |
column and swimlane headings | headingLevel (default 3) |
aria-posinset / aria-setsize |
card | Position in its cell and the cell size (also in virtual cells) |
aria-busy |
card | true while its move is saved |
aria-describedby |
card / column handle | Visually hidden instructions |
aria-expanded |
column toggle / lane toggle | false when collapsed |
aria-pressed |
Move button / column handle | true while a card is picked / the column is picked up |
aria-disabled |
toolbar buttons | true when Move, Undo, or Redo is not possible (buttons stay focusable) |
tabindex |
card | 0 on the roving card, -1 on the others |
live region |
shared live regions | Assertive: picked up, each keyboard move (‘{column}, position 2 of 5’), dropped, cancelled, refused (locked, full column), save failed. Polite: undone, redone. |
One roving tab stop over the cards (the last focused card, else the first). A picked-up card keeps focus while it moves (it is re-focused after each re-render). After a drop or undo, focus stays on the card. Collapsing a column or swimlane moves the roving stop to the first visible card. Virtual cells keep the focused card rendered; if it scrolls out, focus moves to the first card in view.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”| Criterion | Level | Note |
|---|---|---|
| 1.3.1 Info and Relationships | A | |
| 1.4.1 Use of Color | A | |
| 1.4.10 Reflow | AA | two-dimensional board scrolls in its own container |
| 1.4.11 Non-text Contrast | AA | |
| 2.1.1 Keyboard | A | |
| 2.1.2 No Keyboard Trap | A | |
| 2.4.3 Focus Order | A | |
| 2.4.6 Headings and Labels | AA | |
| 2.4.7 Focus Visible | AA | |
| 2.5.7 Dragging Movements | AA | |
| 2.5.8 Target Size (Minimum) | AA | |
| 4.1.2 Name, Role, Value | A | |
| 4.1.3 Status Messages | AA |
Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- WIP state is text (‘3 of 5’, ‘Over limit’), not color alone.
- Locked and saving states are part of the card text (‘, Locked’, ‘, Saving’).
- The single-pointer path: select a card, press Move, then click a card (to place it before) or an empty part of a cell (to place it last).
- Pointer drags are announced at pick-up and drop only.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
Behavior
Section titled “Behavior”States
Section titled “States”Card focused, picked up (keyboard), dragged (pointer), picked for a single-pointer move, saving (aria-busy), locked; column at or over its limit, collapsed, picked up; swimlane collapsed; valid or invalid drop target; empty cell; virtual cell; read-only board.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| card focused | Space | picked up (announced with its position) |
| picked up | arrow keys, Home, End | moved in the preview (each step announced) |
| picked up | Space, Enter | aui-card-move -> applied (announced) -> saving (when moveCard is set) |
| picked up | Escape, Tab | cancelled, back in place |
| saving | moveCard resolves |
saved |
| saving | moveCard rejects or times out |
moved back, removed from history, aui-move-error |
| card focused | toolbar Move | picked; a click on a card or cell moves it there; Escape or Move cancels |
| column header | Space on the handle, arrows, Space | reordered (aui-column-move) |
| any | Ctrl/Cmd+Z / Undo | previous state (saved with reason: 'undo') |
Accessibility notes
Section titled “Accessibility notes”- Moving cards never requires dragging (WCAG 2.5.7): keyboard grab-and-move, or select a card, press Move card, and click the target.
- WIP state, locked cards, and saving cards are text (“3 of 5”, “Over limit”, “, Locked”, “, Saving”), not color alone; the accent stripe is decorative.
- Escape while a card is picked up cancels the move and does not close an enclosing dialog.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
Responsive behavior
Section titled “Responsive behavior”The board scrolls in both directions inside its container (a two-dimensional layout; WCAG 1.4.10 exception) while the page reflows at 320 px. The column header row is sticky. Set column widths with --aui-kanban-column-width. On touch devices drags start from the card grip; touching elsewhere scrolls. Toolbar and column buttons meet touch-target-min.
Right-to-left
Section titled “Right-to-left”With direction: 'rtl' ArrowLeft and ArrowRight swap roles for moving between columns (for cards and column reordering). The layout follows the document direction; collapse chevrons are mirrored.
Theming
Section titled “Theming”Cards use color-surface, color-border, radius-md, shadow-sm, and an accent stripe from color-chart-1..8. Picked cards use color-selected with color-selected-text. Over-limit columns get a color-warning-fg top border, icon, and “Over limit” text. Drop targets use dashed color-focus-ring (valid) or color-error-fg (invalid) borders. Forced colors: CanvasText borders, Highlight for picked cards and valid targets, Mark for invalid targets and over-limit columns.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { kanban: { wipPolicy: 'block', headingLevel: 2, virtualThreshold: 200 } } });Global options: wipPolicy, virtualThreshold, requestTimeoutMs, historyLimit, headingLevel, plus inherited locale and direction. Changes apply to existing boards immediately.
Component configuration
Section titled “Component configuration”Instance options override global defaults. All options are dynamic through update(); cards and columns replace the data and clear the undo history.
grip (Move card, card grip, column handle), undo, redo, alert-triangle (over limit), chevron-down / chevron-right (chevron-left in RTL) for collapse toggles.
Security
Section titled “Security”Titles, descriptions, tags, and labels render through textContent; renderCard content is DOM owned by the application. Options are validated before use. Drag offsets and virtual spacers are written through the CSSOM only (CSP-safe). moveCard receives an AbortSignal and times out.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry of its own. Moves are reported through aui-card-move (with every card after the move) and saved by the application’s moveCard; cards and columns getters return the current state.
Performance
Section titled “Performance”Cards and cells are rendered once and reused by id; a re-render moves only elements that are out of place. A move re-renders the board from the model. Cells above virtualThreshold render the cards in view plus an overscan, with spacers sized from measured card heights (and scroll anchoring on the focused card). Budgets (e2e): create with 8 columns and 1,000 cards < 120 ms; a keyboard move with 1,000 cards < 40 ms; create with one virtual cell of 5,000 cards < 80 ms.
Supported data scale
Section titled “Supported data scale”Tested with 1,000 cards over 8 columns and 5,000 cards in one virtual cell. Columns and swimlanes are not virtualized (tens of columns and swimlanes).
Edge cases
Section titled “Edge cases”- Moving within a full column is always allowed under
wipPolicy: 'block'; moving into it is refused (keyboard moves skip to the next column that accepts the card). - Locked columns accept no cards and release none; locked cards cannot be picked up.
- A card whose save is pending cannot be moved; undo waits for all saves.
- Dropping a card where it started is not a move (no event).
- A card in a collapsed column or swimlane is not focusable;
focusCardexpands them. - Replacing
cardswhile saves are pending aborts them (no rollback). - Cards whose content changes are re-rendered; unchanged cards keep their DOM.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
KANBAN_OPTIONS |
configuration | Empty label; no columns; duplicate ids; cards in unknown columns or swimlanes (or without laneId on a board with swimlanes); invalid limits, colors, tags, flags, or callbacks; moveCard, moveColumn, setCollapsed, focusCard with unknown ids or invalid targets. |
A failed construction leaves the host untouched. Save failures are events (aui-move-error), not exceptions.
Known limitations
Section titled “Known limitations”- Only cards in a cell are virtualized.
- WIP limits count a column across all swimlanes.
- A failed save drops that move from the undo history.
- Pointer drags are announced at pick-up and drop only.
- Manual screen-reader verification is pending.
Unsupported scenarios
Section titled “Unsupported scenarios”- Adding, editing, or deleting cards and columns inside the board.
- Moving several cards at once.
- Nested swimlanes; cards spanning columns.