Work in Progress Limits
Work in progress (WIP) is the number of cards currently in a workflow stage. A limit helps avoid starting more work than a team can finish.
Limit work in progress
In progress allows one task and already contains Build onboarding.
Try it: Try moving research into In progress. Finish onboarding, then try again.
What to notice: The full column blocks the first move and accepts it after a slot is free.
TypeScript source
Install the packages from Getting Started, then use this source in your app.
import type { KanbanPlugin, KanbanConfig } from '@revolist/kanban';import type { WorkCard } from './first-board';
export function moveResearch(kanban: KanbanPlugin<WorkCard>, columnProp: string) { return kanban.moveCards(['task-1'], { columnProp });}export function moveResearchToPlatform(kanban: KanbanPlugin<WorkCard>) { return kanban.moveCards(['task-1'], { columnProp: 'todo', swimlaneId: 'platform' });}export function selectTodoCards(kanban: KanbanPlugin<WorkCard>) { kanban.setSelectedCardIds(kanban.getCards().filter(card => card.status === 'todo').map(card => card.id));}export const workInProgress: Partial<KanbanConfig<WorkCard>> = { columns: [{ prop: 'todo', name: 'To do' }, { prop: 'doing', name: 'In progress', wipLimit: 1 }, { prop: 'done', name: 'Done' }], wipBehavior: 'block',};export const startBeforeDone: Partial<KanbanConfig<WorkCard>> = { rules: { canDrop: ({ cards, targetColumn }) => targetColumn.prop === 'done' && cards.some(card => card.status === 'todo') ? 'Start the task before marking it done.' : true },};export function frenchBoard(config: KanbanConfig<WorkCard>): KanbanConfig<WorkCard> { return { ...config, locale: 'fr-FR', columns: config.columns?.map(column => ({ ...column, name: ({ todo: 'À faire', doing: 'En cours', done: 'Terminé' } as Record<string, string>)[String(column.prop)] ?? column.name })), labels: { ...config.labels, emptyColumn: 'Aucune carte', dropCardHere: 'Déposer une carte ici', moveCancelled: 'Déplacement annulé', editCard: 'Modifier la carte', deleteCard: 'Supprimer la carte' } };}import { defineCustomElements } from '@revolist/revogrid/loader';import { KanbanPlugin, type KanbanConfig } from '@revolist/kanban';import '@revolist/revogrid-pro/styles.css';import '@revolist/kanban/styles.css';
export interface WorkCard { id: string; title: string; status: string; order: number; description?: string; team?: string; owner?: string; priority?: string; tags?: string[]; progress?: number; startDate?: string; endDate?: string;}export const demoCards: WorkCard[] = [ { id: 'task-1', title: 'Research customer needs', description: 'Collect feedback for the next release.', status: 'todo', team: 'product', owner: 'Ana', priority: 'normal', order: 1000 }, { id: 'task-2', title: 'Build onboarding', description: 'Make the first session useful.', status: 'doing', team: 'product', owner: 'Mia', priority: 'high', order: 1000 }, { id: 'task-3', title: 'Improve API errors', description: 'Return actionable validation messages.', status: 'todo', team: 'platform', owner: 'Rui', priority: 'normal', order: 2000 }, { id: 'task-4', title: 'Release checklist', description: 'Ready for the next deployment.', status: 'done', team: 'platform', owner: 'Sam', priority: 'normal', order: 1000 },];export const boardConfig: KanbanConfig<WorkCard> = { columns: [{ prop: 'todo', name: 'To do', size: 210 }, { prop: 'doing', name: 'In progress', size: 210 }, { prop: 'done', name: 'Done', size: 210 }], card: { titleField: 'title' }, cardRowHeight: 88, addCard: false, swimlaneColumn: false,};
export async function configureBoard( grid: HTMLRevoGridElement, config: KanbanConfig<WorkCard> = boardConfig, cards: readonly WorkCard[] = demoCards, extraPlugins: HTMLRevoGridElement['plugins'] = [],) { await defineCustomElements(); grid.plugins = [KanbanPlugin, ...extraPlugins]; // The packaged editor and history install automatically. grid.columns = [{ prop: 'title', name: 'Title' }, { prop: 'status', name: 'Status' }, { prop: 'owner', name: 'Owner' }]; // Cached remote data can hydrate immediately: finish initial grid setup first. grid.kanban = config.remote ? { ...config, remote: false } : config; grid.source = cards.map(card => ({ ...card })); await grid.componentOnReady(); const plugins = await grid.getPlugins(); const kanban = plugins.find((plugin): plugin is KanbanPlugin<WorkCard> => plugin instanceof KanbanPlugin); if (!kanban) throw new Error('Kanban could not start.'); if (config.remote) grid.kanban = config; return kanban;}
// Give your container a width and mount once after it exists in the DOM.export async function createKanbanBoard(container: HTMLElement) { await defineCustomElements(); const grid = document.createElement('revo-grid'); grid.style.cssText = 'display:block;width:100%;height:400px'; container.append(grid); try { const kanban = await configureBoard(grid); return { grid, kanban, destroy() { grid.remove(); } }; } catch (error) { grid.remove(); throw error; }}Try it: choose Move research to In progress: the full column blocks the move. Choose Finish onboarding, then try moving research again. The freed slot now accepts it.
Start with Your First Board; config is its existing configuration.
grid.kanban = { ...config, columns: [ { prop: 'todo', name: 'To do' }, { prop: 'doing', name: 'In progress', wipLimit: 1 }, { prop: 'done', name: 'Done' }, ], wipBehavior: 'block',};WIP behavior
Section titled “WIP behavior”Set a column wipLimit, or a lane wipLimits entry for a more specific bucket limit. wipBehavior defaults to warn:
warnpermits the move and renders/announces the over-limit state.blockrejects the entire proposed move.
Multi-card moves validate the final batch count, not each card independently. A failed card or capacity check cancels the full move.
Counts
Section titled “Counts”Column and swimlane headers distinguish:
- visible count: cards passing RevoGrid filters, Kanban search, and the optional predicate;
- total count: all canonical cards in that bucket.
WIP limits always use total counts so a filter cannot accidentally permit over-capacity work.
In append mode, both counts describe the cards loaded so far. In indexed mode, known placeholder positions participate in total and visible counts, so loading a page does not change an unfiltered header count. Placeholders remain non-interactive, and active filters can exclude positions that cannot match. Enforce server-side rules in the application whenever Kanban does not have the authoritative records needed for the decision.
Limits with server-side pages
Section titled “Limits with server-side pages”A partial board cannot know cards that have not loaded. In append mode, local counts and WIP checks describe loaded cards. Indexed placeholders can reserve known positions, but the server must still enforce full-board capacity and concurrent changes atomically. See load pages and save changes.
Next steps
Section titled “Next steps”Continue with move rules and filtering. See the Kanban API for exact types and signatures.