JavaScript Kanban
@lemonadejs/kanban · ✓ 9 contract checks · framework-agnostic · zero dependencies
<Kanban /> — a drag-and-drop kanban board.
LAYOUT: each column is its own vertical flex stack, so cards flow naturally and uneven card heights never create gaps. (An earlier design placed every card on ONE shared CSS grid to preserve a card’s DOM node across a cross-column move; that shared rows between columns, so a tall card in one column left a gap in another. Cards here are plain, stateless elements — losing the node on a cross-column move costs nothing — so the simpler, gapless per-column layout wins.) Cards are keyed by id WITHIN their column: a reorder inside a column is a keyed move (DOM identity kept); a cross-column move re-parents the card.
- data: [{ id, title, cards: [{ id, title, description?, color?, tags? }] }] BY REFERENCE — the board mutates IN PLACE and calls touch(); your objects stay yours
- drag a card with the mouse: the card lifts OUT of the flow (the others close ranks) and a ghost slot the card’s size previews the exact landing position; Escape cancels mid-drag, events fire ONLY on commit
- keyboard: cards are tabbable; Space (or the ⠿ grip button) picks the focused card up, arrows steer the ghost between slots and columns, Space/Enter drops through the SAME move path a mouse drop commits through, Escape cancels; Enter on an idle card fires oncardclick. While picked, clicking a destination card or a column’s empty space drops there — a single-pointer, no-drag alternative. A polite live region narrates pick/move/drop
- oncardmove(cardId, fromColumnId, toColumnId, index) on any move (drag or api.moveCard); onchange(data) on any data change; oncardclick(card) on click (never after a drag); oncarddblclick(card) on double-click
- oncardmenu(card, event): passing a handler renders a ▾ menu button
on every card — open anything from it (e.g. a
<Contextmenu />with your own options); without a handler the button does not exist - api: addCard(columnId, card), removeCard(cardId), moveCard(cardId, columnId, index), undo(), redo() — every board-initiated change (drag or api) is undoable
Styling: lm-kanban-* classes, themable via CSS custom properties (—lm-kanban-column-width, —lm-kanban-header-height, —lm-kanban-*).
Example
import { html } from 'lemonadejs';
import Kanban from '@lemonadejs/kanban';
const App = (props, { state }) => {
const board = state([
{ id: 'todo', title: 'To do', cards: [
{ id: 'c1', title: 'Write release notes', tags: ['docs'] },
{ id: 'c2', title: 'Fix login redirect', description: 'Safari only', color: '#bd7f40', tags: ['bug'] },
{ id: 'c3', title: 'Design pricing page', color: '#6342a1', tags: ['design'] },
] },
{ id: 'doing', title: 'In progress', cards: [
{ id: 'c4', title: 'Migrate to v6', description: 'Datagrid + Modal', color: '#578163', tags: ['engine'] },
] },
{ id: 'done', title: 'Done', cards: [{ id: 'c5', title: 'Set up CI' }] },
]);
const note = state('Drag a card to another column (Escape cancels), or add one.');
let api;
let next = 6;
return html`<div>
<${Kanban} data="${board}" ref="${(a) => (api = a)}"
oncardmove="${(id, from, to, index) => (note.value = `${id}: ${from} → ${to} at position ${index}`)}"
oncardclick="${(card) => (note.value = 'Clicked: ' + card.title)}" />
<p style="font-size:13px;display:flex;gap:12px;align-items:center">
<button onclick="${() => api.addCard('todo', { id: 'c' + next, title: 'New task ' + next++ })}">Add card</button>
<span>${note}</span>
</p>
</div>`;
};Installation
npm install @lemonadejs/kanban
import Kanban from '@lemonadejs/kanban';
import '@lemonadejs/kanban/style.css';
Three deployment forms, one component:
html`<${Kanban} />` // by value (no registration)
setComponents({ Kanban }); // then <Kanban /> by name anywhere
createWebComponent(Kanban); // <lm-kanban> in plain HTML/any framework
Props
Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.
| Prop | Type | Default | Description |
|---|---|---|---|
data | array | — | KanbanColumn[] BY REFERENCE (mutate + touch()) |
Events
All event names are lowercase (the platform convention — LJS-305 warns otherwise).
oncardmove— (cardId, fromColumnId, toColumnId, index)onchange— (data) — after any board-initiated changeoncardclick— (card) — clicks, never drag commitsoncarddblclick— (card) — double-clicks on a cardoncardmenu— (card, event) — the card ⋮ button (rendered only with a handler)
API
import { ref } from 'lemonadejs';
const kanban = ref();
html`<${Kanban} ref="${kanban}" />`;
// kanban.current.addCard(...) · kanban.current.removeCard(...) · kanban.current.moveCard(...) · kanban.current.undo(...) · kanban.current.redo(...)
addCard()removeCard()moveCard()undo()redo()
Styling
All classes follow the lm-kanban-* convention; visual variants are data-*
attributes on the root. Override freely — there is no styling engine to fight.
Contract
The machine-readable schema ships with the package:
import contract from '@lemonadejs/kanban/contract.json';
verify.json carries the conformance proof produced by verify(Kanban).
Looking for the v5 plugin? See the archived v5 documentation.