| title | Plugin Kanban |
|---|
import { InteractiveDemo } from '@/app/components/InteractiveDemo'; import { PluginLoader } from '@/app/components/PluginLoader';
Kanban board component with drag-and-drop powered by @dnd-kit.
npm install @object-ui/plugin-kanbanThis package publishes a stylesheet. Import it after the base sheets, or the board renders unstyled — the themed utilities it uses have no other source in a published app (#4929):
/* src/index.css */
@import "tailwindcss";
@import "@object-ui/components/style.css";
@import "@object-ui/fields/style.css";
@import "@object-ui/plugin-kanban/style.css";<PluginLoader plugins={['kanban']}>
// Import once in your app entry point
import '@object-ui/plugin-kanban'
import type { KanbanSchema } from '@object-ui/plugin-kanban'
// Use in schemas
const schema: KanbanSchema = {
type: 'kanban',
columns: [
{
id: 'todo',
title: 'To Do',
cards: [
{ id: '1', title: 'Task 1', description: 'Do something' }
]
},
{
id: 'done',
title: 'Done',
cards: []
}
],
onCardMove: (cardId, fromCol, toCol, index) => {
console.log(`Card ${cardId} moved`)
}
}- Drag and drop cards between columns
- Column limits (WIP limits)
- Card badges for status/priority
- Keyboard navigation
- Lazy-loaded (~100-150 KB loads only when rendered)
{
type: 'kanban',
columns?: KanbanColumn[],
onCardMove?: (cardId, fromColumnId, toColumnId, newIndex) => void,
className?: string
}
interface KanbanColumn {
id: string
title: string
cards: KanbanCard[]
limit?: number // Max cards allowed
className?: string
}
interface KanbanCard {
id: string
title: string
description?: string
badges?: Array<{
label: string
variant?: 'default' | 'secondary' | 'destructive' | 'outline'
}>
}
| Property | Type | Description |
|---|---|---|
columns |
KanbanColumn[] | Array of column definitions |
limit |
number | Rows fetched by an object-driven board (default 100) |
onCardMove |
function | Callback when a card is moved |
className |
string | Additional Tailwind CSS classes |
| Property | Type | Description |
|---|---|---|
id |
string | Unique column identifier |
title |
string | Column title |
cards |
KanbanCard[] | Cards in this column |
limit |
number | Max cards allowed (WIP limit) |
className |
string | Additional Tailwind CSS classes |
| Property | Type | Description |
|---|---|---|
id |
string | Unique card identifier |
title |
string | Card title |
description |
string | Card description |
badges |
Badge[] | Status/priority badges |
When a board is bound to an object (an object-kanban, or a kanban view inside
ListView / ObjectView) instead of being given static columns, the fields
rendered on each card resolve in priority order:
- View-level
cardFields— the fields the view configures (a kanban view'scolumns, forwarded ascardFields). An explicit choice always wins. - The object's
highlightFields— the object's ADR-0085 semantic role (its curated "most important fields"), the same list the Grid, List and Detail surfaces already default to. Used when the view configures no card fields, so a board over an object with no per-view config still surfaces meaningful fields. Entries that reference a field the object no longer declares are ignored. - A legacy semantic-field heuristic — a best-effort guess (amount, owner, priority, …) used only when neither of the above is available.
Defaulting to highlightFields keeps a card's contents consistent with the
object's other views without every kanban view having to re-declare its fields.
An object-driven board fetches at most limit records, defaulting to 100. A
board renders every fetched record into a lane and offers no pagination control,
so this is the author's window on the object rather than a page size:
import type { ObjectKanbanSchema } from '@object-ui/types'
const board: ObjectKanbanSchema = {
type: 'object-kanban',
objectName: 'opportunity',
groupBy: 'stage',
limit: 250, // rows fetched; omit for the default 100
}The dataSource binding sets it too — its own limit, or the
pagination.pageSize of the view it names (see
Data source).
import type { KanbanSchema } from '@object-ui/plugin-kanban'
const taskBoard: KanbanSchema = {
type: 'kanban',
columns: [
{
id: 'backlog',
title: 'Backlog',
cards: [
{
id: 'task-1',
title: 'Design new homepage',
description: 'Create mockups for the new landing page',
badges: [
{ label: 'Design', variant: 'default' },
{ label: 'High Priority', variant: 'destructive' }
]
}
]
},
{
id: 'in-progress',
title: 'In Progress',
limit: 3, // WIP limit
cards: [
{
id: 'task-2',
title: 'Implement authentication',
description: 'Add OAuth2 login flow',
badges: [
{ label: 'Backend', variant: 'secondary' }
]
}
]
},
{
id: 'review',
title: 'Code Review',
cards: []
},
{
id: 'done',
title: 'Done',
cards: []
}
],
onCardMove: (cardId, fromCol, toCol, index) => {
// Update backend/state
console.log(`Moved ${cardId} from ${fromCol} to ${toCol}`)
}
}import type { KanbanSchema } from '@object-ui/plugin-kanban'
const ticketBoard: KanbanSchema = {
type: 'kanban',
columns: [
{
id: 'new',
title: 'New Tickets',
cards: [
{
id: 'ticket-1',
title: 'Login not working',
description: 'User cannot log in with Google',
badges: [
{ label: 'Bug', variant: 'destructive' },
{ label: 'P1', variant: 'destructive' }
]
}
]
},
{
id: 'assigned',
title: 'Assigned',
limit: 5,
cards: []
},
{
id: 'resolved',
title: 'Resolved',
cards: []
}
],
className: 'min-h-[600px]'
}import type { KanbanSchema } from '@object-ui/plugin-kanban'
const salesPipeline: KanbanSchema = {
type: 'kanban',
columns: [
{
id: 'leads',
title: 'Leads',
cards: [
{
id: 'lead-1',
title: 'Acme Corp',
description: '$50,000 - Enterprise plan',
badges: [
{ label: 'Hot Lead', variant: 'destructive' }
]
}
]
},
{
id: 'qualified',
title: 'Qualified',
cards: []
},
{
id: 'proposal',
title: 'Proposal Sent',
cards: []
},
{
id: 'won',
title: 'Won',
cards: []
}
]
}Use badges to show card status, priority, or categories:
import type { KanbanCard } from '@object-ui/plugin-kanban'
const card: KanbanCard = {
id: 'task-1',
title: 'Important Task',
badges: [
{ label: 'Frontend', variant: 'default' },
{ label: 'Urgent', variant: 'destructive' },
{ label: 'Reviewed', variant: 'secondary' },
{ label: 'Blocked', variant: 'outline' }
]
}default- Blue badgesecondary- Gray badgedestructive- Red badgeoutline- Outlined badge
Set maximum cards per column to enforce work-in-progress limits:
import type { KanbanColumn } from '@object-ui/plugin-kanban'
const column: KanbanColumn = {
id: 'in-progress',
title: 'In Progress',
limit: 3, // Max 3 cards
cards: [
{ id: 'task-2', title: 'Implement authentication' }
]
}When the limit is reached, the column shows visual feedback.
Handle card movements to update your backend or state:
import { useState } from 'react'
import type { KanbanColumn, KanbanSchema } from '@object-ui/plugin-kanban'
declare function updateCardColumn(cardId: string, toColumnId: string, newIndex: number): Promise<void>
declare function moveCard(columns: KanbanColumn[], cardId: string, toColumnId: string, newIndex: number): KanbanColumn[]
export function useBoard(initialColumns: KanbanColumn[]) {
const [columns, setColumns] = useState<KanbanColumn[]>(initialColumns)
const schema: KanbanSchema = {
type: 'kanban',
columns,
// `onCardMove` returns void, so the callback cannot be awaited by the board:
// start the write and update local state without blocking the drop.
onCardMove: (cardId, fromColumnId, toColumnId, newIndex) => {
// Update database
void updateCardColumn(cardId, toColumnId, newIndex)
// Update local state
setColumns((prev) => moveCard(prev, cardId, toColumnId, newIndex))
},
}
return schema
}The plugin uses lazy loading to optimize bundle size:
- Initial load: ~0.2 KB (entry point)
- Lazy chunk: ~100-150 KB (loaded when kanban is rendered)
- Includes @dnd-kit for drag-and-drop functionality
The kanban board includes:
- Keyboard navigation - Move cards with keyboard
- Screen reader support - ARIA labels for all interactions
- Focus management - Clear focus indicators
import type { KanbanSchema, KanbanCard, KanbanColumn } from '@object-ui/plugin-kanban'
const column: KanbanColumn = {
id: 'todo',
title: 'To Do',
cards: [],
limit: 5
}
const kanbanSchema: KanbanSchema = {
type: 'kanban',
columns: [column]
}