Large Boards
Kanban renders the cards needed for the viewport rather than creating a DOM element for every record. Good sizing and lightweight card content keep scrolling and interaction predictable.
Navigate a larger board
This example contains 1,000 local cards.
Try it: Reveal the last task, then return to the first one.
What to notice: Kanban scrolls to the requested card using its public reveal API.
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: scroll through the larger board and inspect cards beyond the first screen. Stable IDs keep each card connected to the same record as visible rows are recycled.
Native row virtualization
Section titled “Native row virtualization”RevoGrid virtualizes workflow columns and the projected card rows. A cell contains at most one card, avoiding nested virtual scrollers and keeping card focus, drag targets, and row ownership aligned with the grid provider model.
Use cardRowHeight to reserve equal space for every card row. Choose a value that fits the number of content bands in your card template; individual swimlane definitions can override it with height. RevoGrid virtualizes those rows directly, while card identity and canonical rank remain independent of the rendered row index.
Start with Your First Board; these recipes extend its grid and config.
grid.kanban = { columns: config.columns, cardRowHeight: 176, card: { titleField: 'title', descriptionField: 'description' },};Card content is contained within its reserved row, so an undersized custom template cannot overlap the next card. Increase cardRowHeight when adding metadata rows rather than relying on content overflow.
Large-board guidance
Section titled “Large-board guidance”- Use stable primitive IDs and finite numeric ranks.
- Keep render hooks pure and avoid creating network requests, timers, or global listeners inside templates.
- Put expensive derived values on the card record or memoize them outside the template.
- Configure
cardRowHeightto fit the complete managed or custom card shell while keeping every projected row uniform. - For known large server datasets, configure
kanban.remote.totalandplaceholder; unloaded card rows use the same indexed skeleton pattern as Infinity Scroll. - In placeholder mode, known positions participate in unfiltered visible counts and total/WIP counts, while placeholders remain non-interactive. Local filters still evaluate the records currently available to the board.
- Indexed page replacement and unfiltered card moves patch affected projection buckets, keeping interaction work independent from a full 100K projection rebuild.
- Persist committed move batches rather than rewriting the entire dataset for every pointer update.
Virtualization limits rendered card elements to the viewport. Card identity and drag targets remain tied to canonical records as the board scrolls.
Next steps
Section titled “Next steps”Continue with load pages, card design and limits. See the Kanban API for exact types and signatures.