<KanbanView /> turns a flat list of items and a column configuration into a full kanban board. Cards are draggable between columns; when a card moves, onChangeColumn fires so you can persist the change to your backend. The component supports real-time updates through a reloadSubject observable and can render arbitrary per-card detail rows defined in IBoardRow[].
npm install --save react-declarative tss-react @mui/material @emotion/react @emotion/styled
import {
KanbanView,
IBoardColumn,
IBoardRow,
IBoardItem,
} from 'react-declarative';
// Shape of each card's data
interface ILeadRow {
first_name: string;
last_name: string;
email: string;
phone: string;
hire_date: string;
}
// Column type — a union of valid column identifiers
type LeadColumn = 'cold' | 'contact' | 'draft' | 'deal';
// Rows define what to display inside each card
const rows: IBoardRow<ILeadRow>[] = [
{
label: 'Display name',
value: (id, employee) =>
[employee.first_name, employee.last_name].join(' '),
},
{
label: 'Email',
value: (id, employee) => employee.email,
click: (id, data, payload) => payload.openPreview(id),
},
{
label: 'Phone',
value: (id, employee) => employee.phone,
},
{
label: 'Hire date',
value: (id, employee) => employee.hire_date,
},
];
// Columns define the board lanes
const columns: IBoardColumn<ILeadRow, {}, LeadColumn>[] = [
{ color: '#00ACC1', column: 'cold', label: 'Cold', rows },
{ color: '#9C27B0', column: 'contact', label: 'Contact', rows },
{ color: '#FFA000', column: 'draft', label: 'Draft', rows },
{ color: '#2E7D32', column: 'deal', label: 'In a deal', rows },
];
// items is the flat list of cards
const items: IBoardItem<ILeadRow, {}, LeadColumn>[] = [
{
id: 'lead-1',
column: 'cold',
data: {
first_name: 'Alice',
last_name: 'Smith',
email: 'alice@example.com',
phone: '+1 555 0100',
hire_date: '2024-01-15',
},
},
{
id: 'lead-2',
column: 'contact',
data: {
first_name: 'Bob',
last_name: 'Jones',
email: 'bob@example.com',
phone: '+1 555 0101',
hire_date: '2024-02-20',
},
},
];
export const LeadsKanban = () => {
const handleChangeColumn = async (
id: string,
column: LeadColumn,
data: ILeadRow,
) => {
// Persist the move to your backend
await fetch(`/api/leads/${id}`, {
method: 'PATCH',
body: JSON.stringify({ status: column }),
});
};
return (
<KanbanView<ILeadRow, {}, LeadColumn>
sx={{ height: 'calc(100vh - 145px)' }}
columns={columns}
items={items}
onChangeColumn={handleChangeColumn}
/>
);
};
columns — IBoardColumn<Data, Payload, ColumnType>[] (required)
Defines the board lanes. Each column has a column identifier (the value stored on each item to indicate which lane it belongs to), an optional label, an optional color (used for the column header accent), and a rows array that controls which fields are shown inside every card in that lane.
items — IBoardItem<Data, Payload, ColumnType>[] (required)
The flat list of cards to display. Each item has an id, a column value (must match one of the column identifiers in your columns array), and a data object of type Data.
onChangeColumn — (id: string, column: ColumnType, data: Data, payload: any) => void | Promise<void>
Called every time a card is dragged to a different column. id is the card's id, column is the destination column identifier. Use this callback to persist the move.
payload — Payload | (() => Payload)
Arbitrary data forwarded to every IBoardRow.value, IBoardRow.click, and IBoardRow.visible callback. Use it to carry service references or user context.
reloadSubject — TSubject<void>
An observable subject. Emit void on it to force the board to re-fetch and re-render all cards. Use this for real-time updates via WebSocket.
onDataRequest — (initial: boolean) => void
Called when the board mounts (initial = true) and whenever it needs fresh data. Pair with reloadSubject to implement a push-based real-time flow.
cardLabel — React.ReactNode | ((id, data, payload) => React.ReactNode | Promise<React.ReactNode>)
Custom label rendered at the top of each card. Falls back to the item's id when not provided.
disabled — boolean
Prevents all drag-and-drop interactions when true.
sx — SxProps
MUI system styles applied to the board root. Set a fixed height to enable scrolling within columns, e.g. sx={{ height: 'calc(100vh - 64px)' }}.
IBoardRow)Each entry in the rows array of a column controls one line inside every card:
import { IBoardRow } from 'react-declarative';
interface ILead {
name: string;
priority: 'high' | 'low';
}
const rows: IBoardRow<ILead>[] = [
{
label: 'Name',
// value receives (cardId, data, payload) — can be async
value: (id, lead) => lead.name,
},
{
label: 'Priority',
value: (id, lead) => lead.priority.toUpperCase(),
// visible controls whether this row renders at all
visible: (id, lead) => lead.priority === 'high',
// click makes the row interactive
click: (id, lead, payload) => payload.openDetail(id),
},
];
Set the color field on each column definition to apply a colored accent to the column header. Any valid CSS color string works:
const columns: IBoardColumn[] = [
{ column: 'todo', label: 'To Do', color: '#1976d2', rows },
{ column: 'in_progress', label: 'In Progress', color: '#f57c00', rows },
{ column: 'done', label: 'Done', color: '#388e3c', rows },
];
Connect a WebSocket or server-sent events stream and call reload() whenever the backend pushes a change. Use reloadSubject to trigger a re-render from outside the component:
import { KanbanView, useSubject } from 'react-declarative';
export const RealtimeBoard = () => {
const reloadSubject = useSubject<void>();
React.useEffect(() => {
const ws = new WebSocket('wss://api.example.com/leads/stream');
ws.onmessage = () => {
// Trigger board refresh on any incoming message
reloadSubject.next();
};
return () => ws.close();
}, []);
return (
<KanbanView
columns={columns}
items={items}
reloadSubject={reloadSubject}
onChangeColumn={handleChangeColumn}
/>
);
};
Tip: For high-frequency updates, debounce emissions on
reloadSubjectto avoid excessive re-renders. TheuseQueuedActionhook from react-declarative can help serialise incoming WebSocket messages before calling reload.