Skip to content

Cards and Design

A card should answer “what work is this?” at a glance. Start with a title, then add only the context users need before they move or open it. A stable ID and workflow status connect the visual card to its application record.

A minimal card shows a clear title on one surface. The workflow column supplies status context, so repeating status inside every card is optional. Add descriptions and metadata only when users need them to decide what to open or move.

Interactive example

Your first board

Four tasks move through To do, In progress and Done.

Try it: Move the research card, or double-click a card to edit it.

Loading example…

What to notice: Moving a card changes its status; the board keeps the same four tasks.

TypeScript source

Install the packages from Getting Started, then use this source in your app.

first-board.ts
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; }
}
Download first-board.ts
const cards = [{ id: 'task-1', title: 'Research customer needs', status: 'todo', order: 1000 }];
grid.kanban = {
columns: [{ prop: 'todo', name: 'To do' }, { prop: 'done', name: 'Done' }],
card: { titleField: 'title' },
cardRowHeight: 80,
swimlaneColumn: false, // This minimal board does not need a lane-label column.
};
grid.source = cards;

The default record fields are id, status and order. Configure idField, columnField and orderField at the board level if your model uses other names. Keep IDs stable and unique; ranks determine order within a column/lane.

ConfigurationUse
titleField, descriptionFieldPrimary title and optional short explanation.
priorityField, tagsFieldCompact priority and label metadata.
assigneeFieldPeople assigned to the card.
progressFieldOptional completion progress.
startDateField, endDateField, dueDateFieldSchedule or due-date context, formatted with the configured locale/timezone.
colorFieldOptional card accent.

Resolvers such as title(card), description(card), tags(card) and assignees(card) can derive content without changing the stored record. Inspect the Kanban API for their exact types. Default mappings can display matching fields already present on a record. For a title-only view of richer records, use customization.cardContent to render just the title inside the managed shell; the underlying card data stays intact.

Use field mappings before writing a custom renderer. This default card can show the description, priority, tags, assignees, progress and a date range while retaining one managed surface.

Interactive example

Show useful card details

Map task data to the built-in description, assignee, tags, priority, progress and date range.

Try it: Compare the task details with the minimal card example. Double-click a card to edit its title.

Loading example…

What to notice: The native card renders your mapped fields while keeping its managed selection and drag behavior.

TypeScript source

Install the packages from Getting Started, then use this source in your app.

card-design.ts
import type { KanbanConfig } from '@revolist/kanban';
import type { WorkCard } from './first-board';
export const minimalCard: Partial<KanbanConfig<WorkCard>> = {
customization: { cardContent: (h, { card }) => h('strong', undefined, card.title) },
cardRowHeight: 88,
};
export const detailedCard: Partial<KanbanConfig<WorkCard>> = {
card: { titleField: 'title', descriptionField: 'description', priorityField: 'priority', tagsField: 'tags', progressField: 'progress', startDateField: 'startDate', endDateField: 'endDate', assignees: card => [{ id: card.owner, label: card.owner ?? 'Unassigned' }] },
cardRowHeight: 220,
};
export const customContent: NonNullable<KanbanConfig<WorkCard>['customization']> = {
cardContent: (h, { card }) => h('div', { class: 'kanban-doc-card' }, [h('strong', undefined, card.title), h('span', { class: 'kanban-doc-card__meta' }, `${card.owner} · ${card.priority}`)]),
};
// Apply with grid.kanban = { ...grid.kanban, ...minimalCard }.
// Kanban retains ownership of the surrounding drag/selection/keyboard shell.
Download card-design.ts
grid.kanban = {
...config,
cardRowHeight: 220,
card: {
titleField: 'title', descriptionField: 'description',
priorityField: 'priority', tagsField: 'labels', assigneeField: 'owner',
progressField: 'progress', startDateField: 'startDate', endDateField: 'endDate',
dateTimeZone: 'UTC',
},
};

Use progress values from 0 to 100 and valid date values. Date-only due dates such as 2026-10-28 differ from time-qualified instants; see editor date storage. Increase the row height to fit the content you enable.

Interactive example

Give cards a clear design

A compact custom card shows its title, owner and priority.

Try it: Compare custom content with a title-only card.

Loading example…

What to notice: The card content changes while the managed card shell keeps selection and dragging.

TypeScript source

Install the packages from Getting Started, then use this source in your app.

card-design.ts
import type { KanbanConfig } from '@revolist/kanban';
import type { WorkCard } from './first-board';
export const minimalCard: Partial<KanbanConfig<WorkCard>> = {
customization: { cardContent: (h, { card }) => h('strong', undefined, card.title) },
cardRowHeight: 88,
};
export const detailedCard: Partial<KanbanConfig<WorkCard>> = {
card: { titleField: 'title', descriptionField: 'description', priorityField: 'priority', tagsField: 'tags', progressField: 'progress', startDateField: 'startDate', endDateField: 'endDate', assignees: card => [{ id: card.owner, label: card.owner ?? 'Unassigned' }] },
cardRowHeight: 220,
};
export const customContent: NonNullable<KanbanConfig<WorkCard>['customization']> = {
cardContent: (h, { card }) => h('div', { class: 'kanban-doc-card' }, [h('strong', undefined, card.title), h('span', { class: 'kanban-doc-card__meta' }, `${card.owner} · ${card.priority}`)]),
};
// Apply with grid.kanban = { ...grid.kanban, ...minimalCard }.
// Kanban retains ownership of the surrounding drag/selection/keyboard shell.
Download card-design.ts
grid.kanban = {
...config,
customization: {
cardContent: (h, { card, selected }) => h('div', { class: 'issue-content' }, [
h('strong', undefined, card.title),
h('span', { class: 'issue-meta' }, `${card.owner} · ${card.priority}`),
selected ? h('small', undefined, 'Selected') : null,
]),
cardProps: ({ card }) => ({ title: `${card.title}, assigned to ${card.owner}` }),
},
};

cardContent replaces the visible content, while Kanban owns the outer shell, keyboard focus, selection, drag behavior, accessibility and rule states. Render content rather than a second bordered card inside it. Use cardProps for supported extra attributes/classes/styles; keep the managed role, identity and interaction handlers intact. Template callbacks receive the canonical card and projected state; DOM reads are unnecessary.

Kanban owns the outer card shell, focus target, selection state, whole-card drag behavior, rule classes, and accessibility attributes. customization.cardContent replaces everything visible inside it.

customization: {
cardContent: (h, { card, selected, badges }) =>
h('div', { class: 'issue-card' }, [
h('strong', undefined, card.summary),
h('small', undefined, `${card.owner} · ${card.points} points`),
selected ? h('span', null, 'Selected') : null,
...badges.map((badge) => h('span', { class: badge.className }, badge.label)),
]),
cardProps: ({ card }) => ({
class: card.blocked ? 'issue-card-shell--blocked' : '',
title: card.summary,
}),
}

The same customization object supports dragPreview, swimlaneHeader, swimlaneColumnHeader, and emptyState templates. The swimlane-header context reports swimlaneColumnCollapsed; compact mode keeps a managed vertical title badge so labels remain readable. Template contexts use canonical cards and projected metadata; do not query card DOM to recover product state.

Configure card.titleField and card.descriptionField, or provide formatter callbacks. The title defaults to the title field and falls back to the card ID.

Default cards can also render shared progress and assignee content without a custom template:

grid.kanban = {
...config,
card: {
titleField: 'name',
startDateField: 'startDate',
endDateField: 'endDate',
dateTimeZone: 'UTC',
progressField: 'percentDone',
assigneeField: 'owners',
assigneeAvatarField: 'ownerAvatars',
},
};

Assignee and avatar fields accept either scalar values or parallel arrays. The default date range appears directly after the card title and uses the configured Kanban locale and optional dateTimeZone. For fully custom cards, import renderKanbanDateRange, renderKanbanProgress, and renderKanbanAssignees and compose them inside customization.cardContent; the managed shell continues to own focus, selection, accessibility, and drag behavior.

Set the grid’s theme to a matching light/dark RevoGrid theme. Kanban uses shared UI tokens for text, elevated surfaces, borders and accents. A dark page alone does not set the grid’s theme.

revo-grid.issue-board {
--kanban-radius: 8px;
--kanban-card-shadow: none;
--kanban-card-hover-shadow: 0 2px 6px var(--rv-ui-shadow);
}

Keep the idle card quiet and retain visible focus, selected, invalid and drag states. Avoid wrapping the grid in a second bordered container. Adjust cardRowHeight for the intended density; custom content must fit the projected row.

For editor fields and validation, see Card editor. For headers, drag previews, empty states and translations, see Templates and i18n.

Continue with editing cards, themes and headers and labels. See the Kanban API for exact types and signatures.