Kanban
A board of cards in columns, moved by dragging, by keyboard or from a menu.
SourcePackage @weft-js/warpPlayground
Preview
tsx
import { state } from '@weft-js/core'import { Kanban } from '@weft-js/warp'import type { KanbanCardData, KanbanColumn } from '@weft-js/warp'export function Board() { const columns = state(BOARD) return <Kanban label="Release board" columns={columns} height="460px" />}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]type Spend = { id: string; name: string; deploys: number; cost: number; kids?: Spend[] | undefined; hasChildren?: boolean }const leaf = (p: Project): Spend => ({ id: p.id, name: p.name, deploys: p.deploys, cost: p.cost })const team = (name: string, ...ids: string[]): Spend => ({ id: name, name, deploys: 0, cost: 0, kids: PROJECTS.filter(p => ids.includes(p.id)).map(leaf) })const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })const BOARD: KanbanColumn[] = [ { id: 'todo', title: 'To do', color: 8, cards: [ card('t1', 'Rotate the edge cache keys', 'Ops', 2, 'Jun', 'Nov 4'), card('t2', 'Add Apple Pay to the receipts', 'Web', 1, 'Tomás', 'Nov 6'), card('t3', 'Write the ranking rollout notes', 'Docs', 5, 'Lena'), ] }, { id: 'doing', title: 'In progress', color: 1, limit: 3, cards: [ card('t4', 'Retry timed out upstream calls', 'Mobile', 3, 'Sam', 'Nov 2'), card('t5', 'Rank exact matches first', 'Search', 4, 'Lena', 'Nov 3'), ] }, { id: 'review', title: 'In review', color: 4, cards: [card('t6', 'Prorate seats on plan change', 'Billing', 6, 'Mara')] }, { id: 'done', title: 'Done', color: 7, cards: [ card('t7', 'Cache product images at the edge', 'Web', 1, 'Priya'), card('t8', 'Lower the stale window to 30s', 'Ops', 2, 'Jun'), ] },]Installation
npm install @weft-js/warpUsage
tsx
import { Kanban } from '@weft-js/warp'Saving a move
onMove gives you the card, the column and the place, to send to your server.
tsx
import { state } from '@weft-js/core'import { Kanban, Stack, Text } from '@weft-js/warp'import type { KanbanCardData, KanbanColumn } from '@weft-js/warp'export function Moves() { const columns = state(BOARD) const last = state('Move a card. onMove reports where it landed.') const moved = (id: string, column: string, index: number) => (last.value = `${id} moved to ${column}, position ${index + 1}.`) return ( <Stack gap="related"> <Text subtle>{last.value}</Text> <Kanban label="Release board" columns={columns} onMove={moved} height="420px" /> </Stack> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]type Spend = { id: string; name: string; deploys: number; cost: number; kids?: Spend[] | undefined; hasChildren?: boolean }const leaf = (p: Project): Spend => ({ id: p.id, name: p.name, deploys: p.deploys, cost: p.cost })const team = (name: string, ...ids: string[]): Spend => ({ id: name, name, deploys: 0, cost: 0, kids: PROJECTS.filter(p => ids.includes(p.id)).map(leaf) })const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })const BOARD: KanbanColumn[] = [ { id: 'todo', title: 'To do', color: 8, cards: [ card('t1', 'Rotate the edge cache keys', 'Ops', 2, 'Jun', 'Nov 4'), card('t2', 'Add Apple Pay to the receipts', 'Web', 1, 'Tomás', 'Nov 6'), card('t3', 'Write the ranking rollout notes', 'Docs', 5, 'Lena'), ] }, { id: 'doing', title: 'In progress', color: 1, limit: 3, cards: [ card('t4', 'Retry timed out upstream calls', 'Mobile', 3, 'Sam', 'Nov 2'), card('t5', 'Rank exact matches first', 'Search', 4, 'Lena', 'Nov 3'), ] }, { id: 'review', title: 'In review', color: 4, cards: [card('t6', 'Prorate seats on plan change', 'Billing', 6, 'Mara')] }, { id: 'done', title: 'Done', color: 7, cards: [ card('t7', 'Cache product images at the edge', 'Web', 1, 'Priya'), card('t8', 'Lower the stale window to 30s', 'Ops', 2, 'Jun'), ] },]Adding cards
onAdd shows Add card. You add it to your state, so the id is yours.
tsx
import { state } from '@weft-js/core'import { Kanban } from '@weft-js/warp'import type { KanbanCardData, KanbanColumn } from '@weft-js/warp'export function Add() { const columns = state(BOARD.slice(0, 3)) let made = 0 const add = (column: string, title: string) => { const next = { id: `new-${made++}`, title } columns.value = columns.value.map(c => (c.id === column ? { ...c, cards: [...c.cards, next] } : c)) } return <Kanban label="Release board" columns={columns} onAdd={add} height="440px" />}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]type Spend = { id: string; name: string; deploys: number; cost: number; kids?: Spend[] | undefined; hasChildren?: boolean }const leaf = (p: Project): Spend => ({ id: p.id, name: p.name, deploys: p.deploys, cost: p.cost })const team = (name: string, ...ids: string[]): Spend => ({ id: name, name, deploys: 0, cost: 0, kids: PROJECTS.filter(p => ids.includes(p.id)).map(leaf) })const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })const BOARD: KanbanColumn[] = [ { id: 'todo', title: 'To do', color: 8, cards: [ card('t1', 'Rotate the edge cache keys', 'Ops', 2, 'Jun', 'Nov 4'), card('t2', 'Add Apple Pay to the receipts', 'Web', 1, 'Tomás', 'Nov 6'), card('t3', 'Write the ranking rollout notes', 'Docs', 5, 'Lena'), ] }, { id: 'doing', title: 'In progress', color: 1, limit: 3, cards: [ card('t4', 'Retry timed out upstream calls', 'Mobile', 3, 'Sam', 'Nov 2'), card('t5', 'Rank exact matches first', 'Search', 4, 'Lena', 'Nov 3'), ] }, { id: 'review', title: 'In review', color: 4, cards: [card('t6', 'Prorate seats on plan change', 'Billing', 6, 'Mara')] }, { id: 'done', title: 'Done', color: 7, cards: [ card('t7', 'Cache product images at the edge', 'Web', 1, 'Priya'), card('t8', 'Lower the stale window to 30s', 'Ops', 2, 'Jun'), ] },]Over its limit
limit on a column shows the count against it, and warns when it goes over.
tsx
import { state } from '@weft-js/core'import { Kanban } from '@weft-js/warp'import type { KanbanCardData, KanbanColumn } from '@weft-js/warp'export function Limit() { const columns = state(OVER) return <Kanban label="Over its limit" columns={columns} height="360px" />}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string; name: string; owner: string; region: string; status: Status; deploys: number; uptime: number; latency: number; cost: number; updated: string }[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31' }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31' }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30' }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29' }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29' }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27' }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22' }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31' }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28' }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30' },]type Project = (typeof PROJECTS)[number]type Spend = { id: string; name: string; deploys: number; cost: number; kids?: Spend[] | undefined; hasChildren?: boolean }const leaf = (p: Project): Spend => ({ id: p.id, name: p.name, deploys: p.deploys, cost: p.cost })const team = (name: string, ...ids: string[]): Spend => ({ id: name, name, deploys: 0, cost: 0, kids: PROJECTS.filter(p => ids.includes(p.id)).map(leaf) })const latency = (r: number, h: number) => 40 + ((r + 3) * 37 + h * h * 11) % 260const initials = (name: string) => PEOPLE.find(p => p.name.startsWith(name))?.initialsconst card = (id: string, title: string, label: string, color: number, who: string, meta?: string): KanbanCardData => ({ id, title, label, color, assignee: initials(who), meta })const OVER: KanbanColumn[] = [ { id: 'todo', title: 'To do', color: 8, cards: [card('o1', 'Rotate the edge cache keys', 'Ops', 2, 'Jun')] }, { id: 'doing', title: 'In progress', color: 1, limit: 2, cards: [ card('o2', 'Retry timed out upstream calls', 'Mobile', 3, 'Sam'), card('o3', 'Rank exact matches first', 'Search', 4, 'Lena'), card('o4', 'Prorate seats on plan change', 'Billing', 6, 'Mara'), ] },]2,000 cards a column
Each column is a window on its cards, so a long one costs what a short one does.
tsx
import { state } from '@weft-js/core'import { Kanban } from '@weft-js/warp'import type { KanbanColumn } from '@weft-js/warp'export function Long() { const columns = state(SPRINT) return <Kanban label="2,000 cards a column" columns={columns} height="420px" />}const DEPLOYS = [ { id: 'd-4821', project: 'storefront', branch: 'main', commit: '9f3ac21', message: 'Cache product images at the edge', author: 'Priya Raman', status: 'Live', minutes: 2, at: '2026-10-31T16:42' }, { id: 'd-4820', project: 'search-api', branch: 'ranking-v2', commit: '1be07d4', message: 'Rank exact matches first', author: 'Lena Fischer', status: 'Building', minutes: 6, at: '2026-10-31T16:10' }, { id: 'd-4819', project: 'mobile-bff', branch: 'main', commit: 'c44e190', message: 'Retry timed out upstream calls', author: 'Sam Okafor', status: 'Failed', minutes: 4, at: '2026-10-31T15:27' }, { id: 'd-4818', project: 'checkout', branch: 'main', commit: '70d2fa8', message: 'Add Apple Pay to the last step', author: 'Tomás Oliveira', status: 'Live', minutes: 3, at: '2026-10-31T14:03' }, { id: 'd-4817', project: 'edge-cache', branch: 'main', commit: 'e81b6c2', message: 'Lower the stale window to 30s', author: 'Jun Tanaka', status: 'Live', minutes: 1, at: '2026-10-31T11:55' }, { id: 'd-4816', project: 'billing', branch: 'main', commit: '2ad9e03', message: 'Prorate seats on plan change', author: 'Mara Lindqvist', status: 'Live', minutes: 3, at: '2026-10-29T17:20' },]const SPRINT: KanbanColumn[] = ['Backlog', 'Next', 'Doing'].map((title, c) => ({ id: title, title, color: c + 1, cards: Array.from({ length: 2000 }, (_, i) => ({ id: `${title}-${i}`, title: `${DEPLOYS[(i + c) % 6]!.message} (${i + 1})`, meta: DEPLOYS[(i + c) % 6]!.project })),}))When to use
Playground
Height
import { Kanban } from '@weft-js/warp'<Kanban label="Release board" columns={columns} height="460px" />Accessibility
- Alt+arrows
- Moves the focused card
- Space
- Picks up or drops the card
- ←→↑↓
- Moves a card that's picked up
- Esc
- Puts a card back
- There are three ways to move a card, and only one needs a pointer. The Move button lists every destination.
- Every move is announced with where the card landed.
- On a touch screen the Move button opens a sheet.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
columns | KanbanColumn[] | State<KanbanColumn[]> | — | A state, or a list the board follows and onMove reports to. |
height | string | — | The board's height. Default 560px. |
label | string | 'Board' | Names the board. |
| Event | Type | Default | Description |
|---|---|---|---|
onMove | ((id: string, column: string, index: number) => void) | — | Called with the card's id, the column and the index. |
onAdd | ((column: string, title: string) => void) | — | Shows Add card in each column, and is called with the column and the title. |
onDrift | ((index: number, predicted: number, actual: number) => void) | — | Called when a card's measured height differs from the estimate, with both heights. For tuning. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="edges"] [data-part="sr-only"]
sr-only
RelatedTableChecklistVirtual list