Kanban
A board view of columns and cards: drag cards across columns or reorder them within a column, with column reordering, WIP limits, multi-select, and swimlanes on top; every settled move emits oas-change so the host can sync data. Fits staged workflows such as task boards, idea pools, and hiring pipelines.
Basic usage
Both columns (column definitions) and cards are passed as JSON attributes (property arrays work the same). Cards are grouped by their column field and render title by default:
Three-column board
Drag any card onto another column and drop it — the oas-change feedback shows up here.
Drag across columns and reorder
HTML5 DnD: drag a card onto a target column and the drop point shows a primary-color insertion indicator (top/bottom half of a hovered card selects before/after; empty columns highlight the tail). On drop the component writes the card data (new column plus new order) back to cards and emits oas-change with detail { id, from, to, index } — index is the insertion position in the target column (measured after removing the dragged card itself; hosts can reproduce with splice(index, 0, card) on the target array). Dropping back at the original spot is a no-op (no event).
Move and reorder (independently scrolling columns)
Setting height on the component makes each column body scroll independently; drag a card up/down inside a column to reorder.
Column reordering
Use the drag handle on the left of each column header to move whole columns (or focus the handle and press ←/→); the drop point shows a primary-color insertion line between columns. On settle the component writes the new order back to columns and emits oas-column-reorder with detail { from, to, keys } — from is the moved column's original position, to its new one, keys the full reordered list; card data is untouched. Dropping back at the original spot is a no-op. Touch devices (pointer: coarse) get move-left/move-right buttons inside the column header (boundary items disabled):
Column reordering
Drag the handle on a column header (or focus it and press ←/→) — the oas-column-reorder feedback shows up here.
WIP limits
Add limit (a number) to a column definition: when the column's card count exceeds it, the header count turns warning-colored and the column gets a data-over-limit marker. The limit is advisory, not blocking — dropping into an over-limit column still lands; whether to warn is up to the host. Unset, 0, or negative means unlimited:
WIP limit (In progress has limit=2, currently over)
The "In progress" column is 3/2 — over its limit: warning-colored count plus data-over-limit, yet cards can still be dropped in.
Multi-select
Ctrl/Cmd+click toggles cards into the selection; Shift+click selects a range (from the anchor card to the clicked one; both must be in the same column and the same lane band — across columns / lanes it degrades to single-select); selected cards get a primary-color outline. Drag any selected card and all selected cards move together to the drop column, emitting a single oas-change whose detail gains an ids array (id stays the first). Click blank space or press Esc to clear the selection; a plain click falls back to single-select. Touch devices have no multi-select (the move menu acts on its own card only):
Multi-select batch move
Ctrl/Cmd+click a couple of cards in "To do", then drag any of them to a target column — the batch-move feedback shows up here.
Swimlanes
swimlane-by="fieldKey" splits the board into horizontal bands by that field's value: each band has a header row (field value + count + fold arrow; click to fold/unfold, all expanded by default, fold state survives data changes) and its cards are grouped by column inside. Dropping a card into another band rewrites that field's value (oas-change detail gains swimlane: { from, to }); cards missing the value fall into the "(empty)" band and empty cells show a placeholder (bands derive from existing cards — no fully-empty band form). Note: the swimlane-by field name must not collide with the built-in column field (cross-band dragging rewrites that field — a same-named field would destroy column assignment). Hovering a collapsed band during a drag auto-expands it for drop targeting; cancelling the drag keeps it expanded (the user has seen the content — no rollback):
Swimlanes (grouped by priority)
Drag a card into another band (e.g. pull "Unscheduled task" into High) — the swimlane feedback shows up here; click a band header to fold it.
Empty columns
Empty columns show a "Drag cards here" placeholder (translated via locale); the empty-column-text attribute overrides it:
Empty column with custom placeholder
Custom card rendering (renderCard)
Only title renders by default; for richer cards (tags, owners, progress) use the renderCard property function: (card) => Node | string. A returned Node is mounted directly; a returned string renders as plain text (injection-safe). While the function is present the data is not reflected back to the cards attribute (functions cannot be serialized — the host owns the data); dragging still emits oas-change and positions/groups keep updating.
renderCard rich cards
Touch and keyboard
- Touch devices (
pointer: coarse) cannot drive HTML5 drag: every card gets a move button in its top-right corner that opens a small menu — move up / move down / move to a column (move up is disabled on the first card, move down on the last); column headers get move-left/move-right buttons for reordering (boundary items disabled); - Keyboard accessible: cards are focusable (
tabindex=0), non-empty column bodies arerole="list"(empty columns carry no list semantics), cards arerole="listitem", with readable names from column titles and card titles; column reordering also works by focusing the column handle and pressing ←/→; - The move menu acts on its own card only (multi-select does not change menu behavior).
API
oas-kanban
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
cards | Card data (JSON array [{ id, column, title, ...arbitrary fields }] where column is the owning column key). After a drop the component writes the new grouping/order back to this attribute (uncontrolled write-back + attribute reflection); skipped while a renderCard function is present (the host owns the data) | KanbanCard[] | string | [] |
columns | Column definitions (JSON array [{ key, title, limit? }]; the same-named property array channel works identically — the setter reflects back to the attribute). limit is a WIP limit: when the column's card count exceeds it, the header count turns warning-colored and the column gets a data-over-limit marker (advisory only — drops still land); unset/0/negative = unlimited | KanbanColumn[] | string | [] |
empty-column-text | Override for the empty-column / empty swimlane-cell placeholder text (defaults to the i18n kanban.emptyColumn key, translated with the locale) | string | — |
swimlane-by | Swimlane field key: splits the board into horizontal bands by that field's value (band header = fold arrow + field value + count; all expanded by default, fold state survives data changes); missing/empty values fall into the "(empty)" band; dropping a card into another band rewrites the swimlane field value; omit for the plain column layout | string | — |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
renderCard | Custom card renderer (card) => Node | string (property channel; functions cannot be serialized and stay out of attributes/SSR). A returned Node mounts directly; a returned string renders as plain text (injection-safe). While present, card data is not reflected back to the cards attribute (the host owns the data) while drag and the touch move menu still emit oas-change. Set to null to restore the default title rendering | KanbanCardRenderer | null | — |
Events
| Event | Description |
|---|---|
oas-change | Emitted after a drag cross-column move, an in-column reorder, or a touch move-menu action settles (dropping back at the original spot emits nothing). detail { id, from, to, index } — from/to are column keys, index is the insertion position in the target column (measured after removing the moved cards; hosts can reproduce with splice(index, 0, card) on the target array); multi-select drags move all selected cards and emit one event whose detail gains ids (all selected ids in data order, id is the first); cross-swimlane drops also gain swimlane { from, to } (the swimlane field value change) |
oas-column-reorder | Emitted after a column reorder settles (column-head handle drag / keyboard ←/→ / touch move buttons; no-op at the original position emits nothing). detail { from, to, keys } — from is the moved column's original position, to its new position after the reorder, keys the full reordered list of column keys; the component also writes the new order back to the columns attribute (card data is untouched) |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-kanban-column-min-height | 60px |
--oas-kanban-column-min-width | 220px |