Lattice Grid Buy a licence

api reference

Kanban API Reference

createKanban: columns from any status property, drag or keyboard moves that write back, swimlanes, sprints, epics and the child grid.

API reference › The board (kanban) view

All 29 pages Everything on one page → Developer guide →

The board (kanban) view

modules/kanban is an opt-in view of grid rows as cards, grouped into columns by a configurable property - a status, a stage, a state GUID, whatever the host's schema calls it. It is a separate bundle in the same shape as charts and the data router: it adds no weight to a page that does not load it, changes nothing in grid core, and pulls in no dependency. A board is just another dataset viewer: it consumes data through the same keyed-diff contract a grid exposes, board.rows.apply({ add, update, remove }), so a Data Router can attach(value, board) and drive a kanban beside a grid and a chart off one feed. (Distinct from the grid-core board presentation mode, config.board in, which lays a single grid's rows out as lanes; this is a standalone view module.)

import { createKanban } from '@toclocoinc/lattice-grid/modules/kanban';

const board = createKanban(document.querySelector('#board'), {
  rows,                        // or { grid } to bind to a live grid; reuses its column formatters
  rowKey: 'id',
  columnProperty: 'status',    // the group-by property (configurable)
  columns: [                   // shown even when empty; this order is honoured
    { id: 'todo', title: 'To do' },
    { id: 'doing', title: 'In progress', wipLimit: 3 },
    { id: 'done', title: 'Done', color: '#2e7d32' },
  ],
  pointsProperty: 'points',    // configurable; drives the header points sum
  showPoints: true,
  card: { title: 'title', subtitle: 'assignee', labels: 'tags' },
  readonly: { columns: { done: true } },     // granular: whole board / per column / per card
  onCardClick: ({ card }) => open(card.row),
});

Every structural property is named in config - columnProperty, pointsProperty, orderProperty, swimlaneProperty, sprintProperty, epicProperty - so the same board maps any customer schema (a status field, points, sprint, epic, a swimlane) without code change. Configured columns show even when empty; a data value outside them gets its own column rather than being dropped. Each column header carries its card count and, with showPoints, its points sum, and is flagged when the count exceeds a column's wipLimit. Accessibility is built in: the board is a labelled group, each column a labelled list, each card a list item in a roving-tabindex focus ring with arrow-key navigation, and a polite live region is present for the move announcements a later cycle adds.

MemberDescription
createKanban(el, config)Create a board. Pass a DOM element to render into, or null for a headless board that computes the same column/card model without a DOM.
columns() / column(id)The columns with their cards and aggregates, or one column by id (id, title, count, points, wipLimit, over, cards).
count(id) / points(id)A column's card count, and its points sum from pointsProperty.
cards() / card(key)Every card model, or one by its key.
rows.apply({ add, update, remove })The keyed-diff consumer contract a grid shares, so the board is a drop-in Data Router target. Also rows.forEach and rows.count.
move(keys, toColumn, toIndex?)Move one or more cards to a column (and, with orderProperty, to a position within it) - the entry point behind drag-and-drop and keyboard move. Runs the transitions/require workflow rules first (a disallowed or missing-field move fails with a named [lattice] error), then onBeforeMove, then writes through the grid's shipped write-back path.
select / selection / isSelected / clearSelectionCard selection: select(keys, 'set'|'add'|'toggle'|'remove'), the selected keys, a membership test, and a clear. Click selects; Ctrl/Cmd toggles; Shift extends within a column.
collapseColumn(id) / collapseLane(id)Collapse, expand or toggle a column or swimlane; emits column:collapse / swimlane:collapse. State survives a keyed-diff update.
reorderColumns(order) / moveColumn(id, before)Reorder the columns (also done by dragging a column header); emits column:reorder.
setColumns(defs)Replace the whole column set after construction - a tenant renaming, adding or removing a column, in one call, without rebuilding the board. Id-keyed like the grid's own columns.apply(state): a column that keeps its id keeps its cards, its collapsed state and its place in a pinned reorderColumns order; the quick filter and the selection are untouched. A column dropped from defs is not specially handled - a card whose value has nowhere configured to go re-derives a plain, humanised ad hoc column rather than being dropped, the same rule an always-unconfigured value already gets.
setQuickFilter(text) / setFilter(fn) / facets(property)Quick text search across card fields, a predicate filter, and distinct-value counts for a facet control.
filters.where(name, fn) / filters.where(name, null) / filters.where() / filters.reapply(name?)Named predicates, composed with AND - the grid's own filters.where convention. Register or replace one under name; where(name, null) removes only that one, leaving the others in force; where() lists the registered names; reapply(name?) re-runs and re-renders. The quick filter is untouched by any of this. setFilter(fn) is unchanged sugar for where(filters.DEFAULT, fn), so it composes with any other named predicate instead of replacing it.
setSprint(id) / showBacklog() / sprints()Sprint view and switcher: show one sprint, the backlog (board.BACKLOG - cards with no sprint), or all; and the distinct sprint values. Emits sprint:changed.
setEpic(id) / epics() / epicRollup() / rollup(property)Epic view and rollup: filter to an epic, list epics, and roll rows up by epic (or any property) into count, points, and progress toward the done columns.
expand(key) / closeDetail() / canExpand(card)Pop a card's children out as a nested child grid (or board) in a drawer/modal/inline container; emits card:expand and card:drill.
editCard(key, field) / applyEdit(key, field, value)Inline-edit a card field opted in with card: { title: { field, edit: true } }: grid-bound it commits through the grid's own field editor path (grid.edit.setCells); standalone it uses a host editor factory or a default input, reverting when onCardEdit rejects. Double-click a card to edit; emits card:edit.
addCard(columnId, seed?)Add a card to a column (with config.addCard's per-column affordance) and open it in inline edit; a host onAddCard(columnId) supplies the row, or one is generated (grid-bound via grid.edit.addRow). Emits card:add.
getState() / setState(snapshot)Serialise and restore the board state - collapsed columns/lanes, column order, quick filter, sprint/epic selection and selection. Also accepted as config.state at construction.
slaThe card-aging / SLA monitor, present only when a sla config is supplied. Read sla.states(), sla.breaches()/sla.warnings() and sla.stateFor(cardOrKey) for each card's age and level; sla.evaluate() re-checks and fires crossings. See the card-aging note below.
setLoading(bool) / setError(message)A loading state and a host-supplied error banner; empty columns already render their placeholder.
fieldsExtra columns of the bound grid to project onto the rows a tile filter sees, beyond the fields the tiles declare. A read of a bound column outside the projection still resolves, and warns once naming the tile and the column; a field naming no column at all is refused by name at first read, and that tile reports unknown rather than an aggregation identity.
setRows(rows) / refresh()Replace the source rows, or recompute and re-render. What refresh() re-reads depends on where the rows come from: a grid-bound board re-reads the bound grid; a board still on its config.rows re-reads that array (a host that mutated it in place sees the change); a routed board - any row has arrived through rows.apply, typically from a Data Router attach - re-reads nothing: it regroups from the rows it holds and shows exactly what it showed before, re-rendered. setRows puts a routed board back on its configured rows.
on(name, fn) / off(name, fn)Events: card:click, card:dblclick, card:contextmenu, card:move, card:reverted, card:confirmed, card:sla, selection:changed, drag:start, drag:end (plus the vocabulary the later cycles emit). On a grid-bound board a move fires card:move optimistically; the grid's write-back then settles it with card:confirmed or, if the server rejects, card:reverted (the card re-reads and the flow transition log rolls the optimistic move back).
readonly(scope)Whether a scope is readonly - the whole board, a { column } or a { card }. A readonly card is not draggable; a move into a readonly column is refused.
destroy()Empty the element and drop the model. The host still owns any bound grid.

Drag-and-drop and keyboard move (write-back). Cards drag between columns (writing columnProperty) and within a column into a position (writing orderProperty with fractional ranking, so only the moved cards' order is written). The same move is available from the keyboard: focus a card, press Space to grab it, use the arrows to choose a target column and position (announced on a live region), Space/Enter to drop, Escape to cancel. Multi-select drags every selected card. A move calls onBeforeMove(card, from, to, index) first - return false (or a promise of it) to veto - then persists: grid-bound, through grid.edit.setCells (the same public edit-commit path inline editing and the write-back adapter use, so the grid's own pipeline owns the optimistic apply, the confirm and the revert); standalone, optimistically with a revert when onCardMove returns false/rejects. A configurable per-card contextMenu (an array or fn(card, selected)) replaces the card:contextmenu event when present.

Touch. The drag above is a pointer gesture, so cards, column headers and swimlane headers drag on phones and tablets as well as with a mouse. A mouse drag starts on movement; a touch or pen drag starts after a short press-and-hold - moving before the hold scrolls the board instead, so a quick swipe never accidentally moves a card. While a drag is held, the pointer auto-scrolls a board edge it sits within 24 px of, and Escape or a second finger cancels. A completed move still fires the same beforeMove/card:move events and the htmx lattice:kanban-change post, once per move.

Swimlanes, collapse, reorder and search. Set swimlanes: true to render a 2D lane×column grid grouped by swimlaneProperty: one band per lane with its own count/points, columns aligned across every lane, the board scrolling vertically through lanes and horizontally through columns inside its own box. A drag across lanes writes the swimlane property too. Columns and lanes collapse (their state survives a keyed-diff update); columns reorder by dragging their header (reorderColumns/moveColumn). setQuickFilter(text) searches across card fields, setFilter(fn) applies a predicate (sugar for filters.where(filters.DEFAULT, fn) - filters.where(name, fn) registers any number of independent named predicates, ANDed together), and facets(property) returns distinct-value counts to build a facet control. Naming the swimlaneProperty is separate from turning on the lane view, so a board can carry it for a cross-lane move without switching layout.

Sprint, epic and card pop-out. setSprint(id) shows one sprint, showBacklog() the cards with no sprint, and sprints() feeds a switcher; setEpic(id) narrows to an epic and epicRollup() (or rollup(property)) returns per-epic count, points and progress toward the done columns. A card can pop out a nested grid of its children - an epic's stories, a story's tasks, recursively. The child relationship is a childrenProperty (parent-id within the dataset) and/or a loadChildren(card) (per-card dataset or async fetch), and the child is a full composed createGrid (sort/filter/edit/write-back) - supplied as children.factory - opened in a drawer (default), modal or inline. With children.asBoard the child is itself a board, so it can pop its own children. This reuses the grid by composition and adds no grid-core coupling. expand(key) and the per-card drill affordance emit card:expand; a deeper open emits card:drill.

Live updates. Because the board consumes data through the same keyed-diff contract a grid does, a Data Router drives it directly - router.attach(board, predicate) - and one feed fans out to a grid, a kanban, a chart and a KPI tile at once. A live rows.apply({ add, update, remove }) is applied as a keyed diff (an unchanged card keeps its model) and re-rendered preserving scroll, focus, selection, collapsed columns/lanes and any open pop-out, so a card can appear, move or update under the user without losing their place.

Scale, state and accessibility. virtualize renders only a scroll window of a tall column (with true-height spacers so the scrollbar stays honest), for boards of thousands of cards. getState()/setState() (and config.state) save and restore the collapsed columns and lanes, the column order, the quick filter and the sprint/epic selection, so a reopened board comes back as it was; setLoading/setError add loading and error states. Accessibility runs throughout: the board is a labelled group of labelled column lists, cards are a roving-tabindex focus ring (arrows to move focus, Enter to activate), the move is fully keyboard-driven (Space grab, arrows for column/position, Alt+↑/↓ across swimlanes, Space/Enter drop, Escape cancel) with live-region announcements, and every affordance carries a name.

Column width. A board used to take a fixed --lattice-board-column-width (default 280px) on every column, so a three-column board left a wide host mostly empty and a page that computed the token from the viewport overflowed its host with a horizontal scrollbar. The columnWidth option fixes that: 'fill' (the default) shares the board width equally between the columns, never below minColumnWidth (240px), so a board that fits fills its host edge-to-edge and one that does not scrolls at the minimum; a number fixes every column at that many pixels; 'auto' keeps the natural token width. Collapsed columns stay narrow strips and the remaining columns take the freed space, and the board follows its host's resize. Setting --lattice-board-column-width on the host element still overrides. The resolved values are read back at board.columnWidth and board.minColumnWidth.

Card aging / SLA. A sla config ages each card and highlights the ones sitting too long. Thresholds are a raw millisecond count or a { days, hours, … } spec, set globally as { warn, breach }, per column (either sla.columns[id], or a column def's own sla/slaWarn/slaBreach) and per swimlane (sla.lanes[id]); the most specific wins, lane → column → global. basis chooses whether the clock is time-in-current-column (default) or age-on-the-board, resolved from the flow transition log, an enteredProperty/createdProperty timestamp, or arrival. The view puts an age chip on aged cards (showAge: 'always' shows it on every card) and a highlight on breached ones. A rising crossing (ok→warn, warn→breach) fires the card:sla event and the onWarn/onBreach(level, rows) callbacks - the same (signal, rows) shape a Data Router alert route uses, so one handler serves both. It is reached at runtime as board.sla; done-column cards are exempt by default (ignoreDone: false opts them in), and an optional tick re-checks so a card that breaches by simply sitting still still lights up. Example: createKanban(el, { …, sla: { warn: { days: 2 }, breach: { days: 4 }, onBreach: notify } }).

Inline edit and add-card. A field is opted into inline edit with the object mapping form - card: { title: { field: 'title', edit: true } }. Double-clicking a card (or editCard(key, field)) edits it in place: grid-bound, through the grid's own field editor for that column via its public edit path, so the column's parse, validate and optimistic/confirm/revert all run; standalone, through a host editor factory (or a default input), reverting when onCardEdit rejects. A per-column add-card affordance (config.addCard) creates a card carrying the column's group value - from a host onAddCard(columnId), or generated, or appended through grid.edit.addRow when bound - and opens it straight in inline edit on its title, so the user just types. The module imports nothing from the grid's DOM package: grid-bound edits ride the grid's public edit API, standalone edits use the host's editor, so a board-only page never pulls the grid in.

Blocked cards and dependencies. A card can be blocked outright, or blocked by other cards. blockedProperty names a row field that blocks a card - a boolean flag, or a reason string whose non-empty value is the reason the blocked badge shows (with an icon and a warning-tone chip, and an accessible name, so it is never colour alone). blockedByProperty names the field holding the keys of the cards that block this one (a comma-separated string or an array); the card then shows a "blocked by N" list of focusable chips, each one focusing (opening) its blocking card, and flips to a ready state once every blocker is in a done column. A blocked filter - setBlockedFilter(true), or blockedFilter: true at construction - shows only blocked cards (a card with an unresolved blocker counts as blocked). Blocking and unblocking run through setBlocked(key, blocked, reason?) (or toggleBlocked(key)), which raise card:edit with field: 'blocked' like any other edit, and a blocked card is refused any forward move (a move into a column to its right). The flow analytics record blocked intervals and report board.flow.blockedTime(), board.flow.flowEfficiency() (active time / cycle time) and board.flow.blockedTimeByColumn(), so a cycle time that was really time waiting on someone else stops looking like active work.

Blocked cards and dependencies, executed

An explicit block with a reason, a dependency on another card, the blocked filter, and a blocker resolving to ready - headless, run on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 'a', status: 'todo', title: 'A' },
    { id: 'b', status: 'todo', title: 'B', blockedBy: 'a' },          // blocked by card a
    { id: 'c', status: 'doing', title: 'C', blocked: 'Waiting on design' }, // explicit reason
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done', done: true }],
  blockedProperty: 'blocked',
  blockedByProperty: 'blockedBy',
  blockedFilter: true,                                    // start filtered to blocked cards
  card: { title: 'title' },
});

board.setBlockedFilter(false);                            // clear it (only b and c were shown)
const c = board.card('c');
const badge = c.blocked + ' ' + c.blockedReason;         // true Waiting on design
const b = board.card('b');
const deps = b.blockedBy.join(',') + ' ' + b.blocked;    // a true (a is not done yet)

board.setBlockedFilter(true);
const filtered = board.model.cardsByKey.size;            // 2 (b and c)
board.setBlockedFilter(false);

await board.setBlocked('c', false);                      // unblock C
const cleared = board.card('c').explicitBlocked;         // false
await board.toggleBlocked('c', 'oops');                 // re-block C
const toggled = board.card('c').blocked;                 // true

await board.move('a', 'done');                           // resolve B's blocker
const ready = board.card('b').ready;                     // true

board.destroy();
return [badge, deps, filtered, cleared, toggled, ready].join(' | ');

Richer card parts. Beyond title/subtitle/labels, the card mapping takes four built-in parts, each optional and each folded into the card's accessible name: assignee - one or several people (a name, a comma-separated string, or an array of { name, avatar } objects) drawn with the avatar component the Gantt has used since 1.88, image with an initials fallback coloured from the name, and a +N overflow beyond max (default 2); due - a date field shown in the grid's date format when bound, with a due-soon/overdue state and icon from a dueSoon day threshold (card: { due: { field: 'due', dueSoon: 3 } }); progress - a 0..1 fraction or 0..100 value as a thin bar; and checklist - a { done, total } object or two named fields, shown as 3/5. A bound grid's own formatters are reused, and board search matches assignee names. density: 'compact' tightens each card (less padding, no subtitle) so a column fits more cards; the default 'comfortable' keeps the roomier card.

Undo and redo. Every user move (a drag, a reorder, a lane change), add and inline edit is one undo step - a multi-card drag is one step, because it reaches move() once with every key it carries. board.undo() re-runs the inverse through the board's own write path (the workflow rules, the onBeforeMove/onBeforeEdit veto, the WIP rules and the confirm/revert pipeline still run, so a refused undo leaves the card and the history unchanged and logs a catalogued warning); board.redo() re-applies, and canUndo()/canRedo() report the stacks, with a history event ({ canUndo, canRedo, label }) on every change. Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z undo and redo while the board has focus (the shortcut stands aside inside a text field). historyDepth caps the steps kept (default 100, 0 turns undo off). On a grid-bound board the board shares the grid's timeline, so one Ctrl+Z order covers a grid edit and a board move; host loads through rows.apply are never undoable.

A board, grouped and aggregated, executed

A customer-shaped set - statuses as columns, points, an empty configured column, a WIP limit, and one status outside the configured set - grouped headless, then a keyed diff applied through the same rows.apply contract the Data Router drives. Run on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 'a', status: 'todo', points: 3, title: 'Login form' },
    { id: 'b', status: 'doing', points: 5, title: 'OAuth' },
    { id: 'c', status: 'doing', points: 2, title: 'Reset flow' },
    { id: 'd', status: 'done', points: 8, title: 'Audit log' },
    { id: 'z', status: 'archived', points: 1, title: 'Old ticket' }, // outside the configured columns
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [
    { id: 'todo', title: 'To do' },
    { id: 'doing', title: 'In progress', wipLimit: 1 },
    { id: 'done', title: 'Done' },
    { id: 'blocked', title: 'Blocked' },   // configured but empty; still shown
  ],
  pointsProperty: 'points',
  card: { title: 'title' },
});

const todo = board.count('todo') + '/' + board.points('todo');       // 1/3
const doing = board.count('doing') + '/' + board.points('doing');    // 2/7
const over = board.column('doing').over ? 'over' : 'ok';             // over the WIP limit of 1
const extra = board.column('archived') ? 'archived' : 'dropped';     // an out-of-set value is kept

// The keyed-diff consumer contract a Data Router drives: move b to done, drop c.
board.rows.apply({ update: [{ id: 'b', status: 'done', points: 5, title: 'OAuth' }], remove: ['c'] });
const done = board.count('done') + '/' + board.points('done');       // 2/13

board.destroy();
return [todo, doing, over, extra, done].join(' | ');

Move, reorder and veto, executed

The move path drag-and-drop and keyboard move share: a reorder writes the order property with fractional ranking, a cross-column move writes the column property, and onBeforeMove can veto. Run headless on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 'a', status: 'todo', ord: 1, title: 'A' },
    { id: 'b', status: 'todo', ord: 2, title: 'B' },
    { id: 'c', status: 'todo', ord: 3, title: 'C' },
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
  orderProperty: 'ord',
  // Veto any move into 'doing'; allow the rest.
  onBeforeMove: (card, from, to) => to !== 'doing',
});

await board.move('c', 'todo', 0);          // reorder C to the top of todo
const order = board.column('todo').cards.map((x) => x.key).join(',');  // c,a,b

await board.move('a', 'done');             // cross-column move writes the column
const moved = board.card('a').columnId;    // done

await board.move('b', 'doing');            // vetoed by onBeforeMove
const vetoed = board.card('b').columnId;   // still todo

return [order, moved, vetoed].join(' | ');

Workflow rules. Two optional config maps make the board enforce a workflow before a move persists, and both run first - before onBeforeMove, before the write-back, before any confirm or revert. transitions maps a source column id to the destination column ids it may move to (a list, or '*' for any); a source absent from the map may move anywhere, so a board that never configures rules stays exactly as permissive as it was. require maps a destination column id to the row properties a card must carry to enter it ({ field, label?, editor? }). A drag over a disallowed column shows it not droppable (dropEffect none, aria-dropeffect="none") and the keyboard move skips it; a drop into a column whose required fields are missing opens a small form, and the move commits only once the values are filled (cancel reverts). A programmatic board.move honours the same rules and fails with a catalogued [lattice] error naming the rule. Either map may also be declared on a lane (config.lanes[i].transitions / .require), overriding the board per key.

Workflow rules, executed

A transition map that gates movement between columns and a required field on done - the refused programmatic moves name their rule, and a card that already carries the required value moves through. Run headless on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 1, status: 'todo', title: 'A' },
    { id: 2, status: 'doing', title: 'B', deployed: '2026-01-01' },
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
  card: { title: 'title' },
  transitions: { todo: ['doing'], doing: ['done'] },   // todo may only reach doing
  require: { done: [{ field: 'deployed', label: 'Deployed' }] },
});

let transition = 'no';
try { await board.move(1, 'done'); }                       // todo → done is not in todo's map
catch (err) { transition = /kanban\.transition/.test(err.message) ? 'transition' : err.message; }

let required = 'no';
await board.move(1, 'doing');                            // todo → doing is allowed
try { await board.move(1, 'done'); }                  // but done requires `deployed`
catch (err) { required = /kanban\.require/.test(err.message) ? 'require' : err.message; }

const moved = (await board.move(2, 'done')).moved.length;  // 1 - card 2 already carries it

board.destroy();
return [transition, required, moved].join(' | ');

Swimlanes, collapse and search, executed

Lanes with per-lane aggregates, a collapsed column, a reorder, and a quick-filter - all headless, run on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 1, status: 'todo', assignee: 'Ann', points: 3, title: 'Login' },
    { id: 2, status: 'doing', assignee: 'Ann', points: 5, title: 'OAuth' },
    { id: 3, status: 'doing', assignee: 'Bob', points: 2, title: 'Reset' },
    { id: 4, status: 'done', assignee: 'Bob', points: 8, title: 'Audit' },
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
  swimlaneProperty: 'assignee',
  swimlanes: true,
  pointsProperty: 'points',
  card: { title: 'title' },
});

const ann = board.model.lanes.find((l) => l.id === 'Ann');
const lane = 'Ann ' + ann.count + '/' + ann.points;   // Ann 2/8

board.reorderColumns(['done', 'todo', 'doing']);
const order = board.columns().map((c) => c.id).join(',');  // done,todo,doing

board.setQuickFilter('reset');
const shown = board.model.cardsByKey.size;             // 1 (only "Reset" matches)

return [lane, order, shown].join(' | ');

Richer card parts, executed

Assignee avatars, a progress bar, a checklist count, an overdue due date, the compact density and a search that matches an assignee's name - resolved headless, run on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 1, status: 'todo', title: 'Ship', assignee: [{ name: 'Ann' }, 'Bo Chen'], pct: 0.4, ck: { done: 3, total: 5 } },
    { id: 2, status: 'todo', title: 'Late', assignee: ['Cy'], due: new Date(Date.now() - 86400000).toISOString() },
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }],
  density: 'compact',
  card: { title: 'title', assignee: 'assignee', progress: 'pct', checklist: 'ck', due: { field: 'due', dueSoon: 3 } },
});

const card = board.card(1);
const overdue = board.card(2).due.state;                     // overdue
board.setQuickFilter('Ann');                                    // search matches an assignee name
const shown = board.model.cardsByKey.size;                    // 1 (only "Ship")

board.destroy();
return [card.assignees.length, card.progressPct, card.checklist.done + '/' + card.checklist.total, overdue, shown].join(' | ');

Label and badge tones, and a due format, executed

A label or badge tone (a map from value to accent, info, success, warning, danger or neutral, or a function of the row) draws the chip in the theme's variant roles; a due format (a date pattern or a function) sets the chip text. Unmapped values stay neutral and raw - resolved headless, run on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 1, status: 'todo', title: 'Ship', tags: 'bug, ok, misc', due: '2026-03-05T12:00:00' },
    { id: 2, status: 'todo', title: 'Fix', tags: 'bug', due: '2026-03-05T12:00:00' },
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }],
  card: {
    title: 'title',
    labels: { field: 'tags', tone: { bug: 'danger', ok: 'success' } },
    due: { field: 'due', format: 'MMM d', locale: 'en-US' },
  },
});
const tones = board.card(1).labelTones;
const text = board.card(1).due.text;

board.destroy();
const plain = createKanban(null, {
  rows: [{ id: 1, status: 'todo', due: '2026-03-05T12:00:00' }],
  rowKey: 'id', columnProperty: 'status', columns: [{ id: 'todo' }],
  card: { due: { field: 'due', format: (d) => 'due ' + d.getFullYear() } },
});
const fn = plain.card(1).due.text;
plain.destroy();
return [tones.bug, tones.ok, tones.misc || 'none', text, fn].join(' | ');

Column width, executed

The columnWidth and minColumnWidth options resolve to fill (default), a fixed pixel width, auto, and a raised minimum - headless, run on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const base = createKanban(null, {
  rows: [], rowKey: 'id', columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
});

const fixed = createKanban(null, {
  rows: [], rowKey: 'id', columnProperty: 'status',
  columns: [{ id: 'todo' }], columnWidth: 300,
});

const auto = createKanban(null, {
  rows: [], rowKey: 'id', columnProperty: 'status',
  columns: [{ id: 'todo' }], columnWidth: 'auto',
});

const wide = createKanban(null, {
  rows: [], rowKey: 'id', columnProperty: 'status',
  columns: [{ id: 'todo' }], minColumnWidth: 200,
});

return [base.columnWidth, base.minColumnWidth, fixed.columnWidth, auto.columnWidth, wide.minColumnWidth].join(' | ');

Sprint, backlog and epic rollup, executed

A sprint view, the backlog, and an epic rollup with progress toward the done column - headless, run on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 's1', status: 'todo', epic: 'E', sprint: 'S1', points: 3, title: 'Story 1' },
    { id: 's2', status: 'done', epic: 'E', sprint: 'S1', points: 5, title: 'Story 2' },
    { id: 's3', status: 'todo', epic: 'E', sprint: null, points: 2, title: 'Story 3' },
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'done', done: true }],
  pointsProperty: 'points',
  sprintProperty: 'sprint',
  epicProperty: 'epic',
});

board.setSprint('S1');
const inSprint = board.model.cardsByKey.size;   // 2 (s1, s2 are in S1; s3 has no sprint)
board.showBacklog();
const backlog = board.model.cardsByKey.size;    // 1 (s3)
board.setSprint(undefined);

const e = board.epicRollup()[0];
const rollup = e.points + ' ' + e.progress;      // 10 0.5

return [inSprint, backlog, rollup].join(' | ');

Live, driven by a Data Router, executed

One feed, routed to a board through the same keyed-diff contract a grid uses: a snapshot hydrates it and a delta moves a card. Run headless on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');
const { createDataRouter } = await import('../packages/modules/data-router/index.js');

const board = createKanban(null, {
  rows: [], rowKey: 'id', columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'done' }], card: { title: 'title' },
});

const router = createDataRouter({ key: 'kind', rowKey: 'id' });
router.attach(board, 'task');                // the board is a drop-in router target
router.load([
  { id: 't1', kind: 'task', status: 'todo', title: 'T1' },
  { id: 't2', kind: 'task', status: 'done', title: 'T2' },
]);
const hydrated = board.count('todo') + ',' + board.count('done');   // 1,1

router.apply([{ op: 'upsert', row: { id: 't1', kind: 'task', status: 'done', title: 'T1' } }]);
const afterDelta = board.count('todo') + ',' + board.count('done'); // 0,2

router.destroy();
return [hydrated, afterDelta].join(' | ');

Re-bound to a replacement grid, executed

A grid-bound board, pointed at a replacement grid through rebind(grid) - the same call the htmx module's re-bind pass makes after a swap. Run headless on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');
const { createHeadlessGrid } = await import('../packages/core/src/index.js');

const grid = createHeadlessGrid({
  rowKey: 'id', columns: [{ field: 'title' }, { field: 'status' }],
  rows: [{ id: 1, title: 'A', status: 'todo' }],
});
const board = createKanban(null, {
  grid, columnProperty: 'status', columns: [{ id: 'todo' }, { id: 'done' }], card: { title: 'title' },
});
const before = board.cards().map((c) => c.fields.title).join(',');   // A

const replacement = createHeadlessGrid({
  rowKey: 'id', columns: [{ field: 'title' }, { field: 'status' }],
  rows: [{ id: 2, title: 'X', status: 'todo' }],
});
board.rebind(replacement);
const after = board.cards().map((c) => c.fields.title).join(',');    // X

return before + ' | ' + after;

Undo and redo, executed

A move is one undo step: undo() returns the card to its column and position, redo() re-applies it, and canUndo()/canRedo() report the stacks, capped by historyDepth. Run headless on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');

const board = createKanban(null, {
  rows: [
    { id: 'a', status: 'todo', ord: 1, title: 'A' },
    { id: 'b', status: 'todo', ord: 2, title: 'B' },
  ],
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo' }, { id: 'done' }],
  orderProperty: 'ord',
  historyDepth: 5,                    // keep five undo steps (default 100)
});

await board.move('a', 'done');                 // one undo step
const moved = board.card('a').columnId;        // done
const can = (board.canUndo() ? 'yes' : 'no') + '/' + (board.canRedo() ? 'yes' : 'no');  // yes/no

await board.undo();                           // a returns to todo, its position restored
const undone = board.card('a').columnId;       // todo

await board.redo();                           // re-apply the move
const redone = board.card('a').columnId;       // done

board.destroy();
return [moved, can, undone, redone].join(' | ');

A server-side board, executed

A board bound to the same pushdown source a grid takes: the column count and points sum come from an engine GROUP BY, cards load one pageSize at a time (loadMore fetches the next page), and a move writes through the source's update path. Run headless on every build.

const { createKanban } = await import('../packages/modules/kanban/index.js');
const { createPushdownSource } = await import('../packages/core/src/index.js');

const rows = [
  { id: 1, status: 'todo', points: 1, title: 'A' },
  { id: 2, status: 'todo', points: 2, title: 'B' },
  { id: 3, status: 'todo', points: 3, title: 'C' },
  { id: 4, status: 'todo', points: 4, title: 'D' },
  { id: 5, status: 'todo', points: 5, title: 'E' },
  { id: 6, status: 'done', points: 6, title: 'F' },
  { id: 7, status: 'done', points: 7, title: 'G' },
];

// A mock pushdown adapter over rows: pages the fetch, groups the totals.
const adapter = {
  capabilities: {
    filter: true, operators: ['eq', 'and'], quick: true, range: true, total: true, group: true,
    mutate: { update: true, returning: 'row' },
  },
  async execute(query) {
    const tree = query.filters;
    const eq = tree && Array.isArray(tree.conditions) ? tree.conditions.find((c) => c.op === 'eq') : tree;
    const col = eq && eq.value;
    const matched = rows.filter((r) => col === undefined || r.status === col);
    const start = query.range ? query.range.start : 0;
    const end = query.range ? Math.min(query.range.end, matched.length) : matched.length;
    return { rows: matched.slice(start, end), total: matched.length };
  },
  async executeGroupedAggregates(query, groupBy, aggs) {
    const by = new Map();
    for (const r of rows) {
      const keys = groupBy.map((g) => r[g]);
      const k = JSON.stringify(keys);
      if (!by.has(k)) by.set(k, { keys, rows: [] });
      by.get(k).rows.push(r);
    }
    return [...by.values()].map(({ keys, rows: bucket }) => ({
      keys,
      level: groupBy.length,
      values: Object.fromEntries(aggs.map((a) => [a.id,
        a.fn === 'count' ? bucket.length : bucket.reduce((t, r) => t + r[a.col], 0)])),
    }));
  },
  async mutate(op) {
    const r = rows.find((x) => String(x.id) === op.key);
    if (r) Object.assign(r, op.patch);
    return { keys: [op.key], rows: [r] };
  },
};

const source = createPushdownSource({ adapter, pageSize: 2, aggregates: { default: 'engine' }, edit: true });

const board = createKanban(null, {
  source,
  rowKey: 'id',
  columnProperty: 'status',
  columns: [{ id: 'todo', title: 'To do', wipLimit: 2 }, { id: 'done', title: 'Done' }],
  pointsProperty: 'points',
  card: { title: 'title' },
  pageSize: 2,
  transitionLog: [],      // the source-supplied transition log enables flow analytics
});

await new Promise((r) => setTimeout(r, 20));  // the construction load settles

const count = board.count('todo');                // 5 - the engine GROUP BY, not the 2 loaded
const cards = board.column('todo').cards.length;   // 2 - the first page
const over = board.column('todo').over;           // true - 5 > the WIP limit of 2
const more = await board.loadMore('todo');        // true - another page was fetched
const cardsAfter = board.column('todo').cards.length;  // 4
const reloaded = await board.reload();          // reload re-queries the engine totals
const isBoard = reloaded === board;
const flow = board.flowAvailable;                // true - the transitionLog is supplied

board.destroy();
return [count, cards, over, more, cardsAfter, isBoard, flow].join(' | ');

Type reference

Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.

The kanban module

KanbanCard

A card model - one row as it appears on the board. `fields` holds the resolved display text for each mapped card field; `columnId` is the column the card sits in; `points` is the numeric points value (0 when absent). `swimlane`/`sprint`/`epic`/`order` are read from their configured properties and carried for the later cycles that render them.

PropertyTypeDescription
keyunknownThe card's identity, from the board's `rowKey`.
rowKanbanRowThe row behind the card. On a grid-bound board this is the materialised object the board read off the grid, not the grid's own row.
columnIdstring | nullWhich column the card sits in - the stringified group value - or null when the row's group value is null or undefined, which leaves the card unplaced.
pointsnumberThe card's estimate, from `pointsProperty`. Zero when there is no points property or the value is not a finite number.
hasPointsbooleanWhether the points value was a finite number. A card without points contributes nothing to a column's sum.
orderunknownThe raw value of `orderProperty`, which ranks the card inside its column. Undefined when no order property is configured. (optional)
swimlaneunknownThe raw value of `swimlaneProperty`, the lane the card belongs to. Undefined when no swimlane property is configured. (optional)
sprintunknownThe raw value of `sprintProperty`, used by the sprint selection. Undefined when no sprint property is configured. (optional)
epicunknownThe raw value of `epicProperty`, used by the epic selection. Undefined when no epic property is configured. (optional)
fieldsRecord<string, string>The card template's text, one entry per `card` mapping. A mapping that names a grid column is read through that column's own formatter, so it reads exactly as the cell does; anything missing is ''.
blockedbooleanWhether the card is blocked: its `blockedProperty` is set, or it still waits on a `blockedByProperty` blocker that is not in a done column. The flag the blocked filter and the forward-move guard read.
explicitBlockedbooleanWhether the card's own `blockedProperty` marks it blocked - the flag behind the blocked badge and the flow analytics' blocked interval, independent of any blockers.
blockedReasonstringThe reason the card is blocked: the `blockedProperty` value itself when it is a non-empty string, '' when the flag was a bare boolean or no reason was written.
blockedByunknown[]The keys of the cards blocking this one, from `blockedByProperty`. Empty when the card has no blockers.
readybooleanWhether the card is ready: it has blockers and every one of them is now in a done column, and it is not explicitly blocked itself.
assignees{ name: string; avatar?: string | null }[]The assignees resolved from the `assignee` mapping: one entry per person, `{ name, avatar }`. Empty when the mapping is absent or the row has none; the names also feed the quick filter. (optional)
due{ text: string; ts: number | null; state: 'overdue' | 'soon' | null } | nullThe due part: the display text (the grid's date format when bound), the parsed timestamp, and the `'overdue'`/`'soon'` state (null when the value is not a parseable date). Null when the `due` mapping is absent. (optional)
progressPctnumber | nullThe progress percent resolved from the `progress` mapping, clamped to 0..100; null when the value is not a number. (optional)
checklist{ done: number; total: number } | nullThe checklist count resolved from the `checklist` mapping, or null when it is unmapped or neither side is numeric. (optional)

KanbanColumn

A column with its cards and aggregates. `over` is true when `count` exceeds `wipLimit`.

PropertyTypeDescription
idstringThe column's identity - a configured column's `id`, or the stringified group value a data-driven column came from.
titlestringThe heading shown above the column. Defaults to the id, humanised.
colorstring | nullThe accent colour drawn as a dot in the header, as configured. Null when the column def set none.
wipLimitnumber | nullThe work-in-progress limit for this column, or null when it has none. Advisory unless `enforceWip` is on.
collapsedbooleanWhether the column is currently collapsed to a strip - either configured so, or collapsed by the user.
cardsKanbanCard[]The column's cards that survive the board's filters, in arrival order, re-sorted by `orderProperty` when one is configured.
countnumberHow many cards the column holds - the length of `cards`, after filtering.
pointsnumberThe sum of the column's card points; cards without a finite points value add nothing.
overbooleanTrue when the column has a WIP limit and its count exceeds it. The header shows this whether or not `enforceWip` refuses the move.

KanbanEditor

A card field editor handle returned by a host editor factory.

PropertyTypeDescription
elHTMLElementThe editor's element. The board appends it to the card being edited.
MethodSignatureParametersReturnsDescription
focus() => void - => voidCalled straight after the editor is appended, to put the caret where it belongs. Without it the board focuses `el` itself. (optional)
destroy() => void - => voidCalled when the edit commits or is cancelled, so the editor can release what it holds. Optional. (optional)

KanbanRequiredField

A required-field rule: the row property a card must carry before it may enter a column, shown in the fill-in form when missing.

PropertyTypeDescription
fieldstringThe row property path to fill.
labelstringThe form field's human label (host-localised); defaults to the property name. (optional)
MethodSignatureParametersReturnsDescription
editor(ctx: { card: KanbanCard; field: string; value: string; commit: (value: unknown) => void; cancel: () => void }) => KanbanEditorctx: { card: KanbanCard; field: string; value: string; commit: (value: unknown) => void; cancel: () => void }=> KanbanEditorA custom editor factory for the form field; defaults to a text input. Same shape as a card field editor. (optional)

KanbanCardMap

The field-to-property mapping that drives the card template.

PropertyTypeDescription
titleKanbanFieldMapThe card's headline text. (optional)
subtitleKanbanFieldMapA second line under the title. (optional)
labelsKanbanToneFieldMapA comma-separated string rendered as one chip per label; blanks are skipped. (optional)
assigneeKanbanFieldMap | { field: string; max?: number }One or several people, drawn as avatars: the row property holds a single {@link KanbanAssignee}, an array of them, or a comma-separated string of names. Up to `max` (default 2) are drawn, the rest as a `+N` overflow. (optional)
dueKanbanDueMapThe due date, with a due-soon/overdue state and icon. (optional)
coverKanbanFieldMapAn image URL drawn as a cover band across the top of the card. (optional)
progressKanbanFieldMapA progress bar over the card. The value is a 0..1 fraction or a 0..100 percent (a trailing `%` is tolerated); a value that is not a number draws no bar. (optional)
checklistKanbanChecklistMapA checklist count on the card, rendered as `3/5`. See {@link KanbanChecklistMap}. (optional)
badgesKanbanToneFieldMapA single badge chip at the end of the meta row. (optional)
accentKanbanFieldMapA colour for the card's left border. Any CSS colour; an empty value leaves the border plain. (optional)
[field: string]KanbanFieldMap | undefinedAny other card slot the board's theme declares, mapped the same way as the named ones above. `undefined` is the honest value for a slot that is left unmapped.

KanbanEvent

The payload of the three card pointer events - `card:click`, `card:dblclick` and `card:contextmenu`. The board's other events carry their own shapes; {@link KanbanEventPayloads} names one per event.

PropertyTypeDescription
cardKanbanCardThe card the event is about.
columnstring | nullThe id of the column the card is in, or null when it is unplaced.
elunknownThe card's element, for a host that wants to anchor a popover to it. Absent on a headless board. (optional)
originalEventunknownThe DOM event that caused this one, so a host can read modifier keys or call `preventDefault`. (optional)

KanbanSlaConfig

Card-aging / SLA configuration. A card is measured against a `warn` and a `breach` threshold; the view puts an age chip on aged cards and a highlight on breached ones, and a rising crossing fires the `card:sla` event and the matching `onWarn`/`onBreach` callback (signature `(level, rows)`, the Data Router alert handler's). Thresholds resolve most-specific-first: lane → column → global. Reached at runtime as {@link Kanban#sla}.

PropertyTypeDescription
warnKanbanSlaThresholdThe global warn threshold. (optional)
breachKanbanSlaThresholdThe global breach threshold. (optional)
columnsRecord<string, KanbanSlaThreshold | { warn?: KanbanSlaThreshold; breach?: KanbanSlaThreshold }>Per-column overrides by column id (each a threshold or a `{ warn, breach }` pair). (optional)
lanesRecord<string, KanbanSlaThreshold | { warn?: KanbanSlaThreshold; breach?: KanbanSlaThreshold }>Per-swimlane overrides by lane id (each a threshold or a `{ warn, breach }` pair). (optional)
basisSlaAgeingBasis 'column' | 'board'Where the ageing clock starts: `'column'` (default) measures time in the card's current column; `'board'` measures age since the card arrived/was created. (optional)
enteredPropertystringA row property holding the wall-clock time the card entered its column. (optional)
createdPropertystringA row property holding the wall-clock time the card was created. (optional)
ignoreDonebooleanWhether cards in a done column are exempt from ageing (default true). (optional)
useTransitionLogbooleanWhether the flow transition log drives the ageing basis when present (default true). (optional)
showAgeSlaAgeChipVisibility 'always' | 'threshold'Show the age chip on every aged card (`'always'`), or only on warn/breach (`'threshold'`, default). (optional)
ticknumberA re-check interval in ms so a card breaching by sitting still still lights up (0 = off). (optional)
MethodSignatureParametersReturnsDescription
now() => number - => numberA wall-clock epoch clock, injectable for deterministic tests (default `Date.now`). (optional)
onWarn(level: 'warn' | 'breach', rows: KanbanRow[]) => voidlevel: 'warn' | 'breach'
rows: KanbanRow[]
=> voidCalled on a rising crossing to warn level, `(level, rows)` - the router alert handler's shape. (optional)
onBreach(level: 'warn' | 'breach', rows: KanbanRow[]) => voidlevel: 'warn' | 'breach'
rows: KanbanRow[]
=> voidCalled on a rising crossing to breach level, `(level, rows)` - the router alert handler's shape. (optional)

KanbanSlaState

The computed SLA state of one card.

PropertyTypeDescription
keyunknownThe card this ageing state belongs to.
columnIdstring | nullThe column the card was in when it was aged - the thresholds are looked up per column.
laneunknownThe card's swimlane value, for a per-lane threshold. Undefined on a board without swimlanes. (optional)
startnumber | nullThe ageing-clock start epoch (ms), or null when no time source could be resolved.
ageMsnumber | nullThe card's age in ms, or null when unknown.
ageTextstringA short human age label (`2d`, `5h`, …), '' when unknown.
warnMsnumber | nullThe resolved warn threshold in ms, or null.
breachMsnumber | nullThe resolved breach threshold in ms, or null.
levelThresholdLevel | nullThe classified level, or null when the card cannot be aged.
breachedbooleanTrue when `level` is `'breach'`.

KanbanSla

The card-aging / SLA monitor, reached as {@link Kanban#sla} when a `sla` config is supplied. Pure and DOM-free: it computes each card's ageing state from the board's card model and the flow transition log, and the view paints it.

PropertyTypeDescription
configobjectThe normalised SLA config (read-only). (read-only)
MethodSignatureParametersReturnsDescription
sync(): KanbanSla - KanbanSlaRecompute every card's SLA state without emitting anything.
evaluate(opts?: { emit?: boolean }): KanbanSlaState[]opts?: { emit?: boolean }KanbanSlaState[]Recompute and fire `card:sla`/`onWarn`/`onBreach` on each rising crossing.
start(): KanbanSla - KanbanSlaEstablish the baseline, notify on the current state, and start the optional tick.
stateFor(cardOrKey: KanbanCard | unknown): KanbanSlaState | nullcardOrKey: KanbanCard | unknownKanbanSlaState | nullThe SLA state of one card (by card model or key), or null when unknown.
states(): KanbanSlaState[] - KanbanSlaState[]Every card's current SLA state.
breaches(): KanbanSlaState[] - KanbanSlaState[]The cards currently at breach level.
warnings(): KanbanSlaState[] - KanbanSlaState[]The cards currently at warn level (not yet breached).
destroy(): void - voidStop the tick and drop the board subscriptions.

KanbanFlowDurationStats

Distribution stats over a set of durations, in milliseconds - the shape {@link KanbanFlow.cycleTime} and {@link KanbanFlow.leadTime} return. `null` throughout when there is nothing to summarise (`count` is then 0).

PropertyTypeDescription
countnumberHow many durations were summarised.
meannumber | nullThe mean duration.
mediannumber | nullThe median duration; identical to `p50`.
p50number | nullThe 50th percentile (R-7).
p85number | nullThe 85th percentile (R-7).
p95number | nullThe 95th percentile (R-7).
minnumber | nullThe shortest duration.
maxnumber | nullThe longest duration.
stdevnumber | nullThe sample standard deviation.
totalnumberThe sum of every duration.

KanbanFlowCfd

The cumulative-flow series {@link KanbanFlow.cfd} returns: at each bucket boundary, how many cards sat in each column, plus the WIP and done totals.

PropertyTypeDescription
bucketnumberThe bucket size actually used, in milliseconds.
columnsstring[]The board's columns, in display order.
points{ /** The bucket boundary's timestamp (ms epoch). */ at: number; /** Card count per column id at this moment. */ counts: Record<string, number>; /** Cards in a WIP column at this moment. */ wip: number; /** Cards in a done column at this moment. */ done: number; /** Cards in any column at this moment. */ total: number; }[]One point per bucket boundary.

KanbanFlow

Flow analytics over a board's transition log: how long a card takes from start to finish (cycle time), from arrival to finish (lead time), and how the shape of the board has changed over time (the cumulative-flow diagram). Reached as {@link Kanban#flow}.

MethodSignatureParametersReturnsDescription
cycleTime(): KanbanFlowDurationStats - KanbanFlowDurationStatsActive-work-time summary: first-done minus first-start, over cards that finished and passed through a start column.
leadTime(): KanbanFlowDurationStats - KanbanFlowDurationStatsArrival-to-done-time summary: first-done minus arrival, over cards that finished.
cfd(opts?: { bucket?: string | number; from?: number; to?: number }): KanbanFlowCfdopts?: { bucket?: string | number; from?: number; to?: number }KanbanFlowCfdThe cumulative-flow series a stacked-area chart draws - one band per column, growing over time.
blockedTime(): KanbanFlowDurationStats - KanbanFlowDurationStatsThe blocked-time summary: how long each card has spent blocked, over cards with any transition. See {@link KanbanFlowDurationStats}.
flowEfficiency(): KanbanFlowDurationStats - KanbanFlowDurationStatsThe flow-efficiency summary: active time / cycle time per card, as 0..1 fractions over finished cards. See {@link KanbanFlowDurationStats}.
blockedTimeByColumn(): { column: string; blockedMs: number; flowEfficiency: number | null }[] - { column: string; blockedMs: number; flowEfficiency: number | null }[]Blocked time and flow efficiency, aggregated per column. Each card's blocked time is charged to the column it now sits in.

KanbanConfig

Kanban configuration. Every structural property is named here so the same board maps any customer schema (a status field, `points`, `sprint`, `epic`, a swimlane property) without code change. Theme: no `theme` of its own - it inherits. Its colours are the grid's `--lattice-*` tokens, so `data-theme="dark"` on any ancestor (a layout's `theme: 'dark'`, or `<html>`) darkens it with everything else, and the nearest `data-theme` wins. Load the grid stylesheet, which carries the tokens.

PropertyTypeDescription
rowsKanbanRow[]The source rows, one per card. Use this or `grid`, not both - `rows` wins. (optional)
gridunknownA Lattice grid to bind to instead of `rows`: the board reads the grid's displayed rows through its own value pipeline and writes moves back through it. On a bound board `rows.apply` and `setRows` are ignored with a warning. (optional)
sourceSourceConfigA pushdown source the board binds to instead of `rows` or `grid`: the result of `createPushdownSource`. The board asks the source's `fetch` for each column's cards one page at a time (loading the next as the column is scrolled), its `aggregate` for each column's count and points sum as an engine GROUP BY, and its `edit.commit` to persist a move. A `source` wins over `rows` and `grid`; use only one. (optional)
pageSizenumberCards fetched per column page on a source-bound board. Default 50. Only meaningful with `source`. (optional)
transitionLogunknown[]A persisted transition log the board hands to flow analytics on a source-bound board. Without it `flowAvailable` is false and flow features report themselves unavailable rather than deriving from the loaded page. Only meaningful with `source`. (optional)
rowKeystring | ((row: KanbanRow) => string | number)Card identity (a field or fn, returning a string or number); default 'id'. Composite (`string[]`) keys are core-grid-only: a board keys its own card identity from one value, so there is nothing for an array to join into here. (optional)
columnPropertystringThe row property whose value puts a card in a column. Without it (and without `grid` or `columns`) the board warns and shows nothing. (optional)
columnsKanbanColumnDef[]The columns to show, as ids or `{ id, title, color, wipLimit, collapsed, done }`. Configured columns appear even when empty; a group value outside them still gets a column of its own, appended, so no card is dropped. (optional)
columnOrderstring[]Pins the leading column order by id; anything not named keeps its natural position after the pinned ones. A user reorder replaces this. (optional)
pointsPropertystringThe row property summed into each column header's points figure. (optional)
showPointsbooleanShow the points sum in each column and lane header (default false). It needs `pointsProperty` too. (optional)
orderPropertystringThe row property that ranks cards within a column. Set it to make dropping a card at a position meaningful: a move then writes a new order value. Without it cards stay in arrival order. (optional)
swimlanePropertystringThe row property that puts a card in a swimlane. Naming it does not switch on the lane layout - set `swimlanes` for that - but it does let a cross-lane drop write the lane back. (optional)
swimlanesbooleanRender the 2D swimlane layout using `swimlaneProperty` (default false). (optional)
lanes(string | KanbanLaneDef)[]Explicit lane definitions; otherwise lanes come from the distinct swimlane values. (optional)
laneOrderstring[]An explicit lane order by id (also set by a lane-header-drag reorder). (optional)
enforceWipbooleanEnforce `wipLimit` as a hard gate: a move that would exceed it is refused (default false). (optional)
sprintPropertystringThe row property holding a card's sprint, which `setSprint` and the sprint selection filter on. (optional)
epicPropertystringThe row property holding a card's epic, which `setEpic` and the epic selection filter on. (optional)
blockedPropertystringThe row property that blocks a card: a boolean flag, or a reason string whose non-empty value is the reason shown on the blocked badge. Truthy means blocked; empty/false means clear. (optional)
blockedByPropertystringThe row property naming the keys of the cards that block this one - a comma-separated string or an array. Each key must be another card's `rowKey`; the card shows a "blocked by N" list of them, and is ready once they are all done. (optional)
blockedFilterbooleanInitially filter to blocked cards only. The same flag `setBlockedFilter(on)` toggles at runtime. (optional)
sprints(string | { id: unknown; title?: string })[]A configurable sprint dataset: the canonical sprint list (order + titles), shown even when empty. (optional)
sprintunknownThe initially selected sprint id, `Kanban.BACKLOG`, or undefined for all. (optional)
epicunknownThe initially selected epic id, or undefined for all. (optional)
doneColumnsstring[]Column ids that count as "done" for a rollup's progress (also a column def's `done: true`). (optional)
transitionsRecord<string, string[] | '*'>Workflow rules: the moves each source column allows, as a list of destination column ids, or `'*'` for any. A source column absent from the map may move anywhere. A move against the map is refused - a drag shows the target as not droppable and a keyboard move skips it, while a programmatic move fails with a named `[lattice]` error. May also be declared per lane; a lane's entry overrides the board's per key. (optional)
requireRecord<string, KanbanRequiredField[]>Workflow rules: the row properties a card must carry before it may enter each destination column. A missing value opens the small fill-in form on a drag, and fails a programmatic move with a named `[lattice]` error. May also be declared per lane; a lane's entry overrides the board's per key. (optional)
childrenKanbanChildrenCard pop-out: a nested child grid or board (master-detail by composition). (optional)
virtualizeboolean | { rowHeight?: number; overscan?: number; threshold?: number; viewport?: number }Card virtualization for tall columns: true, or `{ rowHeight, overscan, threshold, viewport }`. (optional)
slaKanbanSlaConfigCard aging / SLA highlighting: warn/breach thresholds (globally, per column and/or per lane) that age each card and fire `card:sla` on a rising crossing. Opt-in; reached at runtime as {@link Kanban#sla}. See {@link KanbanSlaConfig}. (optional)
stateobjectA saved board state (from `getState`) to restore on construction. (optional)
addCardbooleanShow a per-column add-card affordance. (optional)
quickFilterstringQuick-filter text matched case-insensitively across card fields. (optional)
cardKanbanCardMapMaps the card template's slots - title, subtitle, labels, assignee, due, cover, progress, badges, accent - to row properties, and opts a slot into inline edit. Any other name is carried as an extra field on `KanbanCard.fields`. (optional)
readonlyKanbanReadonlyBlocks edits and moves: `true` for the whole board, or a map singling out columns and cards by id and key. Default false. (optional)
historyDepthnumberHow many undo steps the board keeps: every move, add and inline edit is one step, a multi-card drag included. Default 100; 0 turns undo off. A grid-bound board ignores this and shares the bound grid's history, so one Ctrl+Z order covers both views. (optional)
ariaLabelstringThe board's accessible name. Defaults to `Board`. (optional)
emptyTextstringThe placeholder shown in a column with no cards. Empty by default, so the module ships no English of its own. (optional)
selectablebooleanWhether card selection is enabled (default true). (optional)
densityKanbanDensity 'compact' | 'comfortable'The card density: `'compact'` tightens each card's padding and drops the subtitle; `'comfortable'` (the default) keeps the roomier card. (optional)
columnWidthKanbanColumnWidth number | 'fill' | 'auto'How wide each column is. A number fixes the width in pixels; `'fill'` (the default) shares the board width equally between the columns, never below `minColumnWidth` - so a board whose columns fit fills its host, and one whose columns do not fit keeps them at the minimum and scrolls; `'auto'` keeps the natural fixed width from the `--lattice-board-column-width` token (default 280px). The CSS token, when set, still overrides the option. Collapsed columns stay narrow strips and the rest take the space; the fill follows host resizes. (optional)
minColumnWidthnumberThe pixel floor `'fill'` never drops a column below (default 240). (optional)
labelsRecord<string, string>Host-localised words for the move announcements (grabbed/moved/dropped/reverted/cancelled). (optional)
contextMenuKanbanMenuItem[] | ((card: KanbanCard, selected: KanbanCard[]) => KanbanMenuItem[])A per-card context menu: items, or `fn(card, selectedCards)` returning items. Suppresses `card:contextmenu`. (optional)
MethodSignatureParametersReturnsDescription
cardRenderer(card: KanbanCard, ctx: { column: KanbanColumn; readonly: boolean; el: HTMLElement; doc: Document }) => string | Node | voidcard: KanbanCard
ctx: { column: KanbanColumn; readonly: boolean; el: HTMLElement; doc: Document }
=> string | Node | voidA custom card template: return an HTML string or a DOM node to own the whole card body. (optional)
onCardEdit(event: { card: KanbanCard; key: unknown; field: string; fieldPath: string; value: unknown }) => boolean | void | Promise<boolean | void>event: { card: KanbanCard; key: unknown; field: string; fieldPath: string; value: unknown }=> boolean | void | Promise<boolean | void>Persist a standalone inline edit; return false or a rejected promise to revert. (optional)
onAddCard(columnId: string) => KanbanRow | Promise<KanbanRow> | voidcolumnId: string=> KanbanRow | Promise<KanbanRow> | voidCreate a card for a column on add-card; return the row to create (with its key), a Promise of that row, or nothing to auto-generate. A rejected Promise creates no card and leaves the board unchanged. (optional)
filter(row: KanbanRow, card: KanbanCard) => booleanrow: KanbanRow
card: KanbanCard
=> booleanA predicate filter over cards; only matching cards are shown. (optional)
onBeforeMove(card: KanbanCard, from: string | null, to: string, index: number | null) => boolean | Promise<boolean>card: KanbanCard
from: string | null
to: string
index: number | null
=> boolean | Promise<boolean>Veto/confirm a move before any write. Return `false` (or a promise of it) to refuse; `from`/`to` are column ids, `index` the target position. (optional)
onCardMove(event: KanbanMoveEvent) => boolean | void | Promise<boolean | void>event: KanbanMoveEvent=> boolean | void | Promise<boolean | void>Persist a move on a standalone (non-grid) board. Return `false` or a rejected promise to revert the optimistic move. On a grid-bound board the grid's write-back pipeline persists instead and this is not called. (optional)
onCardClick(event: KanbanEvent) => voidevent: KanbanEvent=> voidCalled when a card is clicked, with the same payload as the `card:click` event. Both fire: this does not replace a registered handler. (optional)
onCardDblClick(event: KanbanEvent) => voidevent: KanbanEvent=> voidCalled when a card is double-clicked, alongside the `card:dblclick` event. (optional)
onCardContextMenu(event: KanbanEvent) => voidevent: KanbanEvent=> voidCalled on a card's context-menu gesture, alongside the `card:contextmenu` event. A configured `contextMenu` suppresses both. (optional)

KanbanChildren

PropertyTypeDescription
propertystringParent-id property linking child rows to a card within the same dataset. (optional)
presentPresentation 'drawer' | 'modal' | 'inline'Where the pop-out appears (default `drawer`). (optional)
asBoardbooleanMake the child a nested board (recursive) instead of a grid. (optional)
gridOptionsobject | ((card: KanbanCard) => object)Options for the child grid/board - an object or `fn(card)`. (optional)
MethodSignatureParametersReturnsDescription
load(card: KanbanCard) => KanbanRow[] | Promise<KanbanRow[]>card: KanbanCard=> KanbanRow[] | Promise<KanbanRow[]>Per-card child rows, sync or async - an alternative (or addition) to `property`. (optional)
hasChildren(card: KanbanCard) => booleancard: KanbanCard=> booleanWhether a card can be expanded, overriding the property/load inference. (optional)
factory(container: HTMLElement, options: object) => { destroy?: () => void }container: HTMLElement
options: object
=> { destroy?: () => void }The grid factory (a `createGrid`) that builds the child grid. (optional)
render(container: HTMLElement, ctx: { card: KanbanCard; rows: KanbanRow[]; board: Kanban; depth: number }) => (void | (() => void))container: HTMLElement
ctx: { card: KanbanCard; rows: KanbanRow[]; board: Kanban; depth: number }
=> (void | (() => void))Fully custom child render; returns a cleanup function. (optional)
title(card: KanbanCard) => stringcard: KanbanCard=> stringThe pop-out title (default the card title). (optional)

KanbanMenuItem

One context-menu item. `action` receives the card, the selected cards, and the board.

PropertyTypeDescription
labelstringThe item's text in the menu.
disabledbooleanGreys the item out and ignores clicks on it. (optional)
MethodSignatureParametersReturnsDescription
action(ctx: { card: KanbanCard; cards: KanbanCard[]; board: Kanban }) => voidctx: { card: KanbanCard; cards: KanbanCard[]; board: Kanban }=> voidRuns when the item is chosen, with the clicked card, the selected cards (the clicked one when nothing is selected) and the board. Not called on a disabled item. (optional)

KanbanMoveEvent

The payload of a `card:move` (and `card:reverted`) event.

PropertyTypeDescription
keysunknown[]The keys of the cards that actually moved - the ones no `beforeMove` veto refused.
cardsKanbanCard[]The moved cards, in the same order as `keys`.
from(string | null)[]Where each card came from, one column id (or null for unplaced) per moved card.
tostringThe column the cards landed in.
indexnumber | nullThe position asked for within the target column, or null for the end.
ordersnumber[] | nullThe new order values written to `orderProperty`, one per moved card - computed to sit between the neighbours at the drop point. Null when no order property is configured.
originEventOrigin 'api' | 'user' | 'init' | 'ai'Where the move came from: `'user'` for a drag or keyboard move, `'ai'` for an approved AI proposal, `'api'` for `board.move`.
laneunknownThe swimlane the cards were moved to, when the gesture named one and a swimlane property is configured. (optional)

KanbanHistoryStep

The outcome of one board undo/redo step.

PropertyTypeDescription
appliedbooleanWhether the step actually applied; false is a vetoed step that left the stacks untouched.
labelstringThe step's label: `'move'`, `'edit'` or `'add'`.

KanbanHistoryEvent

`history`: the undo/redo stacks moved.

PropertyTypeDescription
canUndobooleanWhether there is a step to undo.
canRedobooleanWhether there is a step to redo.
labelstring | nullThe label of the next undo step, or null when there is none.

KanbanBeforeEvent

The members every cancellable board before-event carries.

PropertyTypeDescription
typestringThe event's own name.
originEventOrigin 'api' | 'user' | 'init' | 'ai'Where the action came from.
defaultPreventedbooleanTrue once any handler has cancelled it. (read-only)
reasonstring | nullThe first reason given to `preventDefault`, or null. (read-only)
MethodSignatureParametersReturnsDescription
preventDefault(reason?: string): voidreason?: stringvoidCancel the pending action; the reason is surfaced on the matching `<action>:cancelled`.

KanbanCardEditEvent

`card:edit` and `beforeEdit`: one field of one card.

PropertyTypeDescription
cardKanbanCardThe card being edited.
keyunknownThat card's key.
fieldstringThe card-spec name of the field.
fieldPathstringThe row property the field writes to.
valueunknownThe value being written.
originEventOrigin 'api' | 'user' | 'init' | 'ai'Where the edit came from.

KanbanBeforeEditEvent

`beforeEdit`: an inline card edit is about to be written.

PropertyTypeDescription
cardKanbanCardThe card being edited.
keyunknownThat card's key.
fieldstringThe card-spec name of the field.
fieldPathstringThe row property the field writes to.
valueunknownThe value that would be written.

KanbanEditCancelledEvent

`edit:cancelled`: a `beforeEdit` handler refused the write.

PropertyTypeDescription
reasonstringThe reason given to `preventDefault`, or `'prevented'`.

KanbanCardAddEvent

`card:add`: a card was appended to a column.

PropertyTypeDescription
columnstringThe column it was added to.
keyunknownThe new card's key - the grid's temporary key on a grid-bound board until the server confirms it.

KanbanBeforeAddEvent

`beforeAdd`: a card is about to be appended.

PropertyTypeDescription
columnstringThe column it would be added to.
seedRecord<string, unknown>The seed values the new row would be built from.

KanbanAddCancelledEvent

`add:cancelled`: a `beforeAdd` handler refused the append.

PropertyTypeDescription
columnstringThe column the card was not added to.
seedRecord<string, unknown>The seed values that were not written.
originEventOrigin 'api' | 'user' | 'init' | 'ai'Where the append came from.
reasonstringThe reason given to `preventDefault`, or `'prevented'`.

KanbanBeforeMoveEvent

`beforeMove`: one card of a move is about to be applied; raised once per card.

PropertyTypeDescription
cardKanbanCardThe card being moved.
keyunknownThat card's key.
fromstring | nullThe column it is in, or null when it is unplaced.
tostringThe column it would land in.
indexnumber | nullThe position asked for within that column, or null for the end.

KanbanMoveCancelledEvent

`move:cancelled`: a `beforeMove` handler refused one card of a move.

PropertyTypeDescription
keysunknown[]The one key that did not move.
cardsKanbanCard[]That one card.
from(string | null)[]The column it is still in.
tostringThe column it would have landed in.
indexnumber | nullThe position that was asked for, or null.
reasonstringThe reason given to `preventDefault`, or `'prevented'`.

KanbanCardRevertedEvent

`card:reverted`: a move did not stick - the bound grid refused the write, an `onCardMove` callback returned false, or the server reverted the cell.

PropertyTypeDescription
keysunknown[]The keys that went back.
cardsKanbanCard[]Those cards.
from(string | null)[]Where each came from.
tostring | nullThe column they are in again, or null when it is not known.
indexnumber | nullThe position that had been asked for, or null.
ordersnumber[] | nullThe order values that had been computed, or null.
originEventOrigin 'api' | 'user' | 'init' | 'ai'Where the move came from, on the two board-side reverts. (optional)
revertedbooleanTrue on the grid-side revert, which reports it explicitly. (optional)
reasonstringWhy the grid reverted the cell, on the grid-side revert. (optional)

KanbanCardConfirmedEvent

`card:confirmed`: the grid confirmed the column write behind an optimistic move.

PropertyTypeDescription
keysunknown[]The key that was confirmed.
cardsKanbanCard[]That card, or an empty array when it has since gone.
tostring | nullThe column it is in, or null when the card has gone.

KanbanSelectionEvent

`selection:changed`: the selected cards changed.

PropertyTypeDescription
keysunknown[]Every selected card key, in selection order.

KanbanColumnCollapseEvent

`column:collapse`: a column was collapsed or expanded.

PropertyTypeDescription
columnstringThe column that moved.
collapsedbooleanTrue when it is now collapsed.

KanbanBeforeColumnChangeEvent

`beforeColumnChange`: a column is about to be collapsed or expanded.

PropertyTypeDescription
columnstringThe column that would move.
collapsedbooleanTrue when it would become collapsed.

KanbanColumnChangeCancelledEvent

`columnChange:cancelled`: a `beforeColumnChange` handler refused it.

PropertyTypeDescription
originEventOrigin 'api' | 'user' | 'init' | 'ai'Where the change came from.
reasonstringThe reason given to `preventDefault`, or `'prevented'`.

KanbanSwimlaneCollapseEvent

`swimlane:collapse`: a swimlane was collapsed or expanded.

PropertyTypeDescription
swimlanestringThe lane that moved.
collapsedbooleanTrue when it is now collapsed.

KanbanOrderEvent

`column:reorder` and `swimlane:reorder`: the order the board draws them in.

PropertyTypeDescription
orderstring[]The ids in their new order, as the rebuilt model holds them.

KanbanBeforeOrderEvent

`beforeColumnReorder` and `beforeLaneReorder`: an order is about to be applied.

PropertyTypeDescription
orderstring[]The ids in the order that was asked for.

KanbanOrderCancelledEvent

`columnReorder:cancelled` and `laneReorder:cancelled`: a handler refused the order.

PropertyTypeDescription
originEventOrigin 'api' | 'user' | 'init' | 'ai'Where the reorder came from.
reasonstringThe reason given to `preventDefault`, or `'prevented'`.

KanbanFilterChangedEvent

`filter:changed`: the quick filter or a named predicate changed.

PropertyTypeDescription
quickFilterstringThe quick-filter text now in force, or undefined when there is none. (optional)
hasFilterbooleanTrue when at least one named predicate is registered.
filtersstring[]The names of the registered predicates.

KanbanSprintChangedEvent

`sprint:changed`: the shown sprint changed.

PropertyTypeDescription
sprintunknownThe sprint now shown: a sprint id, `board.BACKLOG`, or undefined for all of them.

KanbanEpicChangedEvent

`epic:changed`: the shown epic changed.

PropertyTypeDescription
epicunknownThe epic now shown, or undefined for all of them.

KanbanCardExpandEvent

`card:expand`: a card's children were opened.

PropertyTypeDescription
cardKanbanCardThe card that was expanded.
rowsRecord<string, unknown>[]Its child rows, as the children loader returned them.
presentPresentation 'drawer' | 'modal' | 'inline'How they are being presented.

KanbanCardDrillEvent

`card:drill`: a card was expanded from inside an already-open detail.

PropertyTypeDescription
cardKanbanCardThe card that was expanded.
rowsRecord<string, unknown>[]Its child rows.
depthnumberHow many levels down this is; 1 is the first drill.

KanbanDragStartEvent

`drag:start`: a card drag began.

PropertyTypeDescription
keysunknown[]Every key the drag carries - the selection when the dragged card is in it.
cardKanbanCardThe card under the pointer.
originalEventunknownThe DOM `dragstart` event.

KanbanDragEndEvent

`drag:end`: a card drag ended, whether or not it dropped on a column.

PropertyTypeDescription
keysunknown[]The keys the drag carried.
originalEventunknownThe DOM `drop` event; absent when the browser ended the drag without one. (optional)

KanbanSlaEvent

`card:sla`: a card crossed an ageing threshold.

PropertyTypeDescription
keyunknownThe card's key.
cardKanbanCard | nullThe card, or null when it is no longer on the board.
levelThresholdLevel | nullThe level it has just reached.
previousThresholdLevel | nullThe level it was at before this crossing.
ageMsnumber | nullIts age in ms, or null when unknown.
ageTextstringA short human age label (`2d`, `5h`, …).
columnIdstring | nullThe column it was aged in.
laneunknownIts swimlane value, on a board with swimlanes. (optional)
warnMsnumber | nullThe resolved warn threshold in ms, or null.
breachMsnumber | nullThe resolved breach threshold in ms, or null.
atnumberThe epoch (ms) of the crossing.

KanbanCardsLoadedEvent

The payload of the `cards:loaded` event: a page of cards has been fetched from the bound pushdown source for one column (and, in swimlane mode, one lane).

PropertyTypeDescription
columnstringThe column the page loaded into.
lanestringThe lane the page loaded into, present only in swimlane mode. (optional)
keysunknown[]The keys of the rows admitted by this page.
morebooleanTrue when another page may still follow for this column (and lane).

KanbanEventPayloads

What a handler receives, per board event.

PropertyTypeDescription
card:clickKanbanEventThe card, its column, its element and the DOM event.
card:dblclickKanbanEventThe card, its column, its element and the DOM event.
card:contextmenuKanbanEventThe card, its column, its element and the DOM event.
card:moveKanbanMoveEventWhich cards moved, where from and to, and the order values written.
card:revertedKanbanCardRevertedEventWhich cards went back, and why when the grid said so.
card:confirmedKanbanCardConfirmedEventThe card the grid confirmed, and the column it is in.
selection:changedKanbanSelectionEventEvery selected card key.
column:collapseKanbanColumnCollapseEventThe column, and whether it is now collapsed.
card:addKanbanCardAddEventThe column added to, and the new card's key.
drag:startKanbanDragStartEventThe keys the drag carries, the card under the pointer, and the DOM event.
drag:endKanbanDragEndEventThe keys the drag carried, and the DOM event where there was one.
swimlane:collapseKanbanSwimlaneCollapseEventThe lane, and whether it is now collapsed.
swimlane:reorderKanbanOrderEventThe lane ids in their new order.
column:reorderKanbanOrderEventThe column ids in their new order.
filter:changedKanbanFilterChangedEventThe quick-filter text, and the named predicates in force.
sprint:changedKanbanSprintChangedEventThe sprint now shown.
epic:changedKanbanEpicChangedEventThe epic now shown.
card:expandKanbanCardExpandEventThe card, its child rows, and how they are presented.
card:drillKanbanCardDrillEventThe card, its child rows, and how deep the drill is.
card:editKanbanCardEditEventThe card, the field, and the value written.
card:slaKanbanSlaEventThe crossing: the level reached, the one before it, and the age behind it.
cards:loadedKanbanCardsLoadedEventThe column (and lane) a page of cards loaded into, the admitted keys, and whether another page follows.
beforeMoveKanbanBeforeMoveEventThe card about to move, with `preventDefault` to stop it.
beforeAddKanbanBeforeAddEventThe column and seed about to be appended, with `preventDefault` to stop it.
beforeEditKanbanBeforeEditEventThe field about to be written, with `preventDefault` to stop it.
beforeLaneReorderKanbanBeforeOrderEventThe lane order about to be applied, with `preventDefault` to stop it.
beforeColumnReorderKanbanBeforeOrderEventThe column order about to be applied, with `preventDefault` to stop it.
beforeColumnChangeKanbanBeforeColumnChangeEventThe collapse about to be applied, with `preventDefault` to stop it.
move:cancelledKanbanMoveCancelledEventThe one card that did not move, and why.
add:cancelledKanbanAddCancelledEventThe card that was not added, and why.
edit:cancelledKanbanEditCancelledEventThe write that was not made, and why.
laneReorder:cancelledKanbanOrderCancelledEventThe lane order that was not applied, and why.
columnReorder:cancelledKanbanOrderCancelledEventThe column order that was not applied, and why.
columnChange:cancelledKanbanColumnChangeCancelledEventThe collapse that was not applied, and why.
historyKanbanHistoryEventThe undo/redo stacks moved.

KanbanRows

The keyed-diff consumer surface a board shares with a grid, so a Data Router routes to it directly.

PropertyTypeDescription
countnumberHow many rows the board holds, before filtering. (read-only)
MethodSignatureParametersReturnsDescription
apply(change: { add?: KanbanRow[]; update?: KanbanRow[]; remove?: unknown[] }): voidchange: { add?: KanbanRow[]; update?: KanbanRow[]; remove?: unknown[] }voidApply a keyed diff: `add` replaces whatever row each key names, `update` merges its fields into the stored row (so a patch need only carry what changed), `remove` drops the keys. The board regroups and repaints, keeping scroll, focus, selection, collapse and any open pop-out. Ignored with a warning on a grid-bound board.
forEach(fn: (row: KanbanRow, key: unknown) => void): voidfn: (row: KanbanRow, key: unknown) => voidvoidVisit every row the board holds, with its key.

KanbanFilters

Named card predicates, composed with AND, following the grid's `filters.where` convention. Several may be registered under different names at once; each can be replaced or removed without touching the others. `setFilter(fn)` is unchanged sugar for `where(DEFAULT, fn)` / `where(DEFAULT, null)`.

PropertyTypeDescription
DEFAULTstringThe reserved name `board.setFilter` registers/removes under. (read-only)
MethodSignatureParametersReturnsDescription
where(): string[] - string[]The registered names, in registration order.
where(name: string, predicate: (row: KanbanRow, card: KanbanCard) => boolean): Kanbanname: string
predicate: (row: KanbanRow, card: KanbanCard) => boolean
KanbanRegister or replace the predicate under `name`.
where(name: string, predicate: null): Kanbanname: string
predicate: null
KanbanRemove whatever is registered under `name`; a no-op if nothing was.
engine(name: string, condition: Condition | FilterGroup): Kanbanname: string
condition: Condition | FilterGroup
KanbanRegister, replace, remove (`null`) or list (no arguments) a named **engine** filter: a pushable condition tree the board sends to a bound pushdown source as a WHERE predicate, so the engine narrows both the column counts and the loaded cards. The tree is never evaluated client-side; on a board with no `source` it is stored but does not narrow cards.
engine(name: string, condition: null): Kanbanname: string
condition: null
KanbanRemove the engine filter registered under `name`; a no-op if nothing was.
reapply(name?: string): booleanname?: stringbooleanRe-run every named predicate (or one, by name) and re-render.

Kanban

A board instance: a kanban view of grid rows as cards grouped into columns. It consumes data through the same keyed-diff `rows.apply` contract a grid exposes, so `dataRouter.attach(value, board)` drives it like any other viewer.

Properties
PropertyTypeDescription
elunknown | nullThe element the board renders into, or null for a headless board. (read-only)
rowKeystring | ((row: KanbanRow) => string | number)The resolved card identity; see `KanbanConfig.rowKey`. (read-only)
rowsKanbanRowsThe keyed-diff consumer surface, the same shape a grid exposes - this is what makes a board a Data Router target.
slaKanbanSlaThe card-aging / SLA monitor, present only when a `sla` config was supplied. (optional)
flowKanbanFlowFlow analytics over the board's transition log: cycle time, lead time and the cumulative-flow diagram. Built automatically unless the config says `flow: false`. (optional)
flowAvailablebooleanWhether flow analytics can answer on this board. False on a source-bound board with no source `transitionLog`, where the flow features report themselves unavailable rather than deriving from the loaded page. (read-only)
sourceBoundbooleanWhether the board is bound to a pushdown source and owns its own paged data path: counts/sums from an engine GROUP BY, cards fetched per column page, moves written through the source. (read-only)
columnWidthKanbanColumnWidth number | 'fill' | 'auto'The resolved column-width mode; see {@link KanbanConfig.columnWidth}. (read-only)
minColumnWidthnumberThe resolved minimum column width in px; see {@link KanbanConfig.minColumnWidth}. (read-only)
filtersKanbanFiltersNamed card predicates, composed with AND. See {@link KanbanFilters}.
BACKLOGunknownThe sentinel `setSprint` value that selects the backlog (cards with no sprint). (read-only)
Methods
MethodSignatureParametersReturnsDescription
columns(): KanbanColumn[] - KanbanColumn[]The current columns in display order, each with its cards and aggregates.
column(id: string): KanbanColumn | undefinedid: stringKanbanColumn | undefinedOne column by id, or undefined when the board has no such column.
count(id: string): numberid: stringnumberHow many cards a column holds after filtering; 0 for an unknown column.
points(id: string): numberid: stringnumberA column's points sum; 0 for an unknown column.
cards(): KanbanCard[] - KanbanCard[]Every card on the board, unplaced ones included, in no particular column order.
card(key: unknown): KanbanCard | undefinedkey: unknownKanbanCard | undefinedOne card by key, or undefined when no card has that key (it may have been filtered out).
on(name: KanbanEventName, fn: (event: KanbanEventPayloads[KanbanEventName]) => void): () => voidname: KanbanEventName
fn: (event: KanbanEventPayloads[KanbanEventName]) => void
() => voidRegister an event handler; returns a function that removes it. An unrecognised event name is warned about once, because it names a binding that could never fire. What each event carries is {@link KanbanEventPayloads}; the handler is declared with the widest of them, so narrow on the name inside it.
off(name: KanbanEventName, fn: (event: KanbanEventPayloads[KanbanEventName]) => void): voidname: KanbanEventName
fn: (event: KanbanEventPayloads[KanbanEventName]) => void
voidRemove a handler registered with `on`.
readonly(scope?: { column?: string; card?: unknown }): booleanscope?: { column?: string; card?: unknown }booleanWhether editing is blocked - for the whole board, or for the column or card named in the scope.
move(keys: unknown | unknown[], toColumn: string, toIndex?: number | null, toLane?: string, opts?: { origin?: 'user' | 'ai' }): Promise<{ moved: unknown[]; reverted: boolean }>keys: unknown | unknown[]
toColumn: string
toIndex?: number | null
toLane?: string
opts?: { origin?: 'user' | 'ai' }
Promise<{ moved: unknown[]; reverted: boolean }>Move one or more cards to a column (and, with an order property, to a position within it), through the workflow rules, the `onBeforeMove` veto and the grid's shipped write-back path. The single entry point behind drag-and-drop and keyboard move. A move the workflow rules refuse rejects with a named `[lattice]` error rather than silently skipping - the interactive paths pre-check the same rules, so only a programmatic call can raise it.
undo(): Promise<KanbanHistoryStep | null> - Promise<KanbanHistoryStep | null>Undo the last user change, re-running its inverse through the board's write path (so a `beforeMove`/`beforeEdit` veto still applies). A refused undo leaves the card and the history unchanged, with a catalogued warning. On a grid-bound board this delegates to the grid's history, so one undo order covers a grid edit and a board move.
redo(): Promise<KanbanHistoryStep | null> - Promise<KanbanHistoryStep | null>Redo the last undone change, re-running it through the board's write path.
canUndo(): boolean - booleanWhether there is a step to undo.
canRedo(): boolean - booleanWhether there is a step to redo.
selection(): unknown[] - unknown[]The selected card keys.
isSelected(key: unknown): booleankey: unknownbooleanWhether a card is selected.
select(keys: unknown | unknown[], mode?: 'set' | 'add' | 'toggle' | 'remove'): Kanbankeys: unknown | unknown[]
mode?: 'set' | 'add' | 'toggle' | 'remove'
KanbanChange the selection: `set` (replace), `add`, `toggle` or `remove`.
clearSelection(): Kanban - KanbanClear the selection.
collapseColumn(id: string, collapsed?: boolean): Kanbanid: string
collapsed?: boolean
KanbanCollapse, expand or toggle a column (emits `column:collapse`).
collapseLane(id: string, collapsed?: boolean): Kanbanid: string
collapsed?: boolean
KanbanCollapse, expand or toggle a swimlane (emits `swimlane:collapse`).
reorderColumns(order: string[]): Kanbanorder: string[]KanbanReorder the columns to the given id order (emits `column:reorder`).
moveColumn(id: string, beforeId: string | null): Kanbanid: string
beforeId: string | null
KanbanMove one column before another (or to the end); emits `column:reorder`.
reorderLanes(order: string[]): Kanbanorder: string[]KanbanReorder the swimlanes to the given id order (emits `swimlane:reorder`).
moveLane(id: string, beforeId: string | null): Kanbanid: string
beforeId: string | null
KanbanMove one swimlane before another (or to the end); emits `swimlane:reorder`.
setFilter(fn: ((row: KanbanRow, card: KanbanCard) => boolean) | null): Kanbanfn: ((row: KanbanRow, card: KanbanCard) => boolean) | nullKanbanSet a predicate filter over cards, or clear it with null. Sugar for `filters.where(filters.DEFAULT, fn)`.
setQuickFilter(text: string): Kanbantext: stringKanbanSet the quick-filter text matched across card fields. Independent of every `filters.where` predicate.
facets(property: string): { value: unknown; count: number }[]property: string{ value: unknown; count: number }[]Distinct values of a property with card counts - the raw material for a facet control.
setSprint(sprint: unknown): Kanbansprint: unknownKanbanSelect the shown sprint (`BACKLOG` for the backlog, undefined for all); emits `sprint:changed`.
showBacklog(): Kanban - KanbanShow only the backlog (cards with no sprint).
setEpic(epic: unknown): Kanbanepic: unknownKanbanSelect the shown epic (undefined for all); emits `epic:changed`.
sprints(): unknown[] - unknown[]The distinct sprint values (the switcher's options); a configured `sprints` dataset pins the order.
sprintDefs(): { id: unknown; title: string }[] - { id: unknown; title: string }[]The sprint dataset as `{ id, title }` descriptors - the configured list plus any data-only sprint.
epics(): unknown[] - unknown[]The distinct epic values.
rollup(property: string): { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[]property: string{ value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[]Roll rows up by a property: per-bucket count, points, done and progress.
epicRollup(): { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[] - { value: unknown; count: number; points: number; doneCount: number; donePoints: number; progress: number }[]The epic rollup (empty when no epic property is configured).
canExpand(card: KanbanCard): booleancard: KanbanCardbooleanWhether a card can be expanded to a child pop-out.
expand(key: unknown): Promise<object | null>key: unknownPromise<object | null>Open a card's children in a pop-out (drawer/modal/inline); emits `card:expand`/`card:drill`.
closeDetail(): Kanban - KanbanClose any open card pop-out.
isFieldEditable(name: string): booleanname: stringbooleanWhether a mapped card field is opted into inline edit and writable.
editCard(key: unknown, name?: string): object | nullkey: unknown
name?: string
object | nullStart inline editing a card's field (the grid's own field editor when bound); no-op headless.
applyEdit(key: unknown, name: string, value: unknown): Promise<boolean>key: unknown
name: string
value: unknown
Promise<boolean>Commit an inline edit through the write-back path (grid.edit.setCells when bound); emits `card:edit`.
setBlocked(key: unknown, blocked: boolean, reason?: string): Promise<boolean>key: unknown
blocked: boolean
reason?: string
Promise<boolean>Block or unblock a card: write its `blockedProperty` - a reason string or `true` to block, `false` to clear - and emit `card:edit` with `field: 'blocked'`, like any edit. A `beforeEdit` veto or an `onCardEdit` returning false refuses/reverts the write.
toggleBlocked(key: unknown, reason?: string): Promise<boolean>key: unknown
reason?: string
Promise<boolean>Toggle a card's blocked flag; sugar for {@link Kanban#setBlocked} reading the card's current state.
setBlockedFilter(on: boolean): Kanbanon: booleanKanbanShow only blocked cards (`true`) or every card (`false`); emits `filter:changed`.
addCard(columnId: string, seed?: KanbanRow): unknown | Promise<unknown>columnId: string
seed?: KanbanRow
unknown | Promise<unknown>Add a card to a column and open it in inline edit; emits `card:add`. Returns the new key directly, or a Promise of it when `onAddCard` returns a Promise or a `beforeAdd` handler defers; a rejected `onAddCard` Promise resolves this to `null` with no card added.
getState(): object - objectSerialise the restorable state: collapsed columns/lanes, order, filter, sprint/epic, selection.
setState(snapshot: object): Kanbansnapshot: objectKanbanRestore a state snapshot from {@link Kanban#getState}.
setLoading(loading: boolean): Kanbanloading: booleanKanbanMark the board loading (renders a host-localised loading state).
setError(message: string | null): Kanbanmessage: string | nullKanbanSet (or clear with null) an error state, rendered as a host-supplied message.
setRows(rows: KanbanRow[]): Kanbanrows: KanbanRow[]KanbanReplace the source rows and re-render, and make that array the board's source again so a later `refresh()` re-reads it. Ignored with a warning on a grid-bound board.
setColumns(defs: KanbanColumnDef[]): Kanbandefs: KanbanColumnDef[]KanbanReplace the board's configured column set. Keeps card placement and interaction state (collapsed columns, column order, quick filter, selection) for every column id that survives; a dropped id is not specially handled - a card whose value has nowhere configured to go re-derives an ad hoc column rather than becoming `unplaced` (the same "never silently drop a card" rule an unconfigured value already gets).
refresh(): Kanban - KanbanRe-read the source and re-render: a bound grid's rows, or the configured array. Once rows have arrived through `rows.apply` nothing is re-read - the board regroups what it holds, so a routed feed is never thrown away.
reload(): Promise<Kanban> - Promise<Kanban>Reload a source-bound board from its pushdown source: drop the loaded pages and re-query the engine's column counts/sums, then fetch the first page of every column. On a client-side board this is `refresh()`. Called automatically at construction and after a search or filter change.
loadMore(columnId: string, laneId?: string): Promise<boolean>columnId: string
laneId?: string
Promise<boolean>Fetch the next page of cards for a column (and, in swimlane mode, a lane) on a source-bound board. The column scroll hook calls this as the user nears the bottom; a no-op resolving `false` when the board is not source-bound, the column is exhausted, or a page is in flight.
rebind(grid: Grid): Kanbangrid: GridKanbanPoint this board at a replacement grid. The one call a page framework makes after its swap destroyed the grid the board was bound to: the board keeps its element, columns and interaction state, drops its subscriptions to the dead grid, subscribes to the new one and re-reads its rows. A no-op on a board not built over a grid, or already following the grid it is handed.
destroy(): void - voidEmpty the element, remove only the class the board added, and stop the flow and SLA monitors. A bound grid is left alone - the host owns it.
Events
EventWhenPayloadCancellable
card:clickA card was clicked, or Enter was pressed on a focused card.KanbanEventno
card:dblclickA card was double-clicked.KanbanEventno
card:contextmenuA context menu was requested on a card.KanbanEventno
card:moveOne or more cards were moved to a column, after every `beforeMove` gate passed and the write-back was applied.KanbanMoveEventno
card:revertedA move did not stick: the bound grid refused the write, an `onCardMove` callback returned false, or the server reverted the cell.KanbanCardRevertedEventno
card:confirmedThe bound grid confirmed the column write behind an optimistic move.KanbanCardConfirmedEventno
selection:changedThe set of selected cards changed.KanbanSelectionEventno
column:collapseA column was collapsed or expanded.KanbanColumnCollapseEventno
card:addA card was appended to a column.KanbanCardAddEventno
drag:startA card drag began.KanbanDragStartEventno
drag:endA card drag ended, whether or not it dropped on a column.KanbanDragEndEventno
swimlane:collapseA swimlane was collapsed or expanded.KanbanSwimlaneCollapseEventno
swimlane:reorderThe swimlane order changed.KanbanOrderEventno
column:reorderThe column order changed.KanbanOrderEventno
filter:changedThe quick filter or a named card predicate changed.KanbanFilterChangedEventno
sprint:changedThe shown sprint changed.KanbanSprintChangedEventno
epic:changedThe shown epic changed.KanbanEpicChangedEventno
card:expandA card's children were opened.KanbanCardExpandEventno
card:drillA card was expanded from inside an already-open detail.KanbanCardDrillEventno
card:editAn inline card edit was written.KanbanCardEditEventno
card:slaA card crossed an ageing threshold - ok to warn, or ok/warn to breach.KanbanSlaEventno
cards:loadedA page of cards was fetched for a column (and lane), carrying the admitted keys and whether another page follows.KanbanCardsLoadedEventno
beforeMoveOne card of a move is about to be applied; raised once per card, and cancellable.KanbanBeforeMoveEventyes
beforeAddA card is about to be appended; cancellable.KanbanBeforeAddEventyes
beforeEditAn inline card edit is about to be written; cancellable.KanbanBeforeEditEventyes
beforeLaneReorderA swimlane reorder is about to be applied; cancellable.KanbanBeforeOrderEventyes
beforeColumnReorderA column reorder is about to be applied; cancellable.KanbanBeforeOrderEventyes
beforeColumnChangeA column collapse or expand is about to be applied; cancellable.KanbanBeforeColumnChangeEventyes
move:cancelledA `beforeMove` handler refused one card; raised once per refused card.KanbanMoveCancelledEventno
add:cancelledA `beforeAdd` handler refused the append.KanbanAddCancelledEventno
edit:cancelledA `beforeEdit` handler refused the write.KanbanEditCancelledEventno
laneReorder:cancelledA `beforeLaneReorder` handler refused the order.KanbanOrderCancelledEventno
columnReorder:cancelledA `beforeColumnReorder` handler refused the order.KanbanOrderCancelledEventno
columnChange:cancelledA `beforeColumnChange` handler refused the collapse or expand.KanbanColumnChangeCancelledEventno
historyThe undo/redo stacks moved.KanbanHistoryEventno