Skip to content

Kanban Board

Data displaySince 0.7.0
import { Kanban, type KanbanCard, type KanbanColumn } from '@absoluteui/components/kanban';
import '@absoluteui/components/kanban/style.css';

Using Angular? See Kanban Board for Angular.

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.

  • 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.
  • 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.
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);
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;
},
});
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)}` : '');
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;
});
}

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.

<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)
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).

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.
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.
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.
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.
Area Description
renderCard Custom, non-interactive card content.

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).
Slot Default icon
move / grip / column handle grip
undo undo
redo redo
over limit alert-triangle
collapse chevron-down / chevron-right

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

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 }

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.

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)
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.

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.

  • 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.

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.

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')
  • 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.

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.

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.

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.

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.

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.

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.

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.

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.

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).

  • 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; focusCard expands them.
  • Replacing cards while saves are pending aborts them (no rollback).
  • Cards whose content changes are re-rendered; unchanged cards keep their DOM.
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.

  • 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.
  • Adding, editing, or deleting cards and columns inside the board.
  • Moving several cards at once.
  • Nested swimlanes; cards spanning columns.