# Kanban URL: /docs/web/components/kanban A drag-and-drop kanban board with sortable columns and items, column reordering, overlay previews, and final drop zones for archiving or completing tasks. **Demo:** ```tsx 'use client'; import { useCallback, useMemo, useState } from 'react'; import { Kanban, KanbanBoard, KanbanColumn, KanbanColumnHandle, KanbanFinalColumn, KanbanFinalZone, KanbanItem, KanbanItemTitle, KanbanOverlay } from '@docyrus/ui/components/kanban'; import { cn } from '@docyrus/ui/primitives/lib/utils'; import { PropControls, type PropControl } from '@/components/prop-controls'; // ─── Types ─────────────────────────────────────────────────────────────────── interface Task { id: string; title: string; priority: 'low' | 'medium' | 'high'; assignee: string; tag?: string; } // ─── Data ──────────────────────────────────────────────────────────────────── const PRIORITY_STYLES: Record ) : {task.title}}
{task.tag && ( {task.tag} )} {task.priority}
{task.assignee}
); } // ─── KanbanDemo ────────────────────────────────────────────────────────────── export function KanbanDemo() { const [columns, setColumns] = useState(INITIAL_TASKS); const [archivedCount, setArchivedCount] = useState(0); const [showDone, setShowDone] = useState(true); const [showArchive, setShowArchive] = useState(true); const [showCounts, setShowCounts] = useState(true); const [flatCursor, setFlatCursor] = useState(false); const [open, setOpen] = useState(false); const [clickedItem, setClickedItem] = useState(null); const taskMap = useMemo(() => { const map = new Map(); for (const items of Object.values(columns)) { for (const item of items) { map.set(item.id, item); } } return map; }, [columns]); const handleFinalDrop = useCallback((item: Task, finalColumnId: string | number) => { const targetColumn = String(finalColumnId); setColumns(prev => ({ ...prev, [targetColumn]: [...(prev[targetColumn] ?? []), item] })); if (targetColumn !== 'done') { setArchivedCount(prev => prev + 1); } }, []); const controls: PropControl[] = [ { type: 'boolean', name: 'showDone', label: 'Done Zone', value: showDone, onChange: setShowDone }, { type: 'boolean', name: 'showArchive', label: 'Archive Zone', value: showArchive, onChange: setShowArchive }, { type: 'boolean', name: 'showCounts', label: 'Counts', value: showCounts, onChange: setShowCounts }, { type: 'boolean', name: 'flatCursor', label: 'Flat Cursor', value: flatCursor, onChange: setFlatCursor } ]; return (
{clickedItem && (
Item clicked: {taskMap.get(clickedItem)?.title ?? clickedItem}
)}
{items.map(task => ( ))} {items.length === 0 && (

No tasks

)}
); })} {(showDone || showArchive) && ( )} {showArchive && ( )} )} ); } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-kanban ``` **Dependencies:** - [@dnd-kit/sortable](https://www.npmjs.com/package/@dnd-kit/sortable) - [@dnd-kit/core](https://www.npmjs.com/package/@dnd-kit/core) - [@dnd-kit/utilities](https://www.npmjs.com/package/@dnd-kit/utilities) - [@radix-ui/react-slot](https://www.npmjs.com/package/@radix-ui/react-slot) - [@types/react](https://www.npmjs.com/package/@types/react) ## Usage ```tsx import { Kanban, KanbanBoard, KanbanColumn, KanbanColumnHandle, KanbanItem, KanbanItemHandle, KanbanItemTitle, KanbanOverlay, KanbanFinalZone, KanbanFinalColumn } from '@docyrus/ui/components/kanban'; const [columns, setColumns] = useState({ todo: [{ id: '1', title: 'Task 1' }, { id: '2', title: 'Task 2' }], done: [{ id: '3', title: 'Task 3' }] }); {items.map((item) => ( ))} ))} ``` ### With Final Drop Zones Use `finalColumns` on `Kanban` and `KanbanFinalZone` + `KanbanFinalColumn` to create drop targets that remove items from columns (e.g., archive or complete). ```tsx ``` ### With Custom Overlay The `KanbanOverlay` children can be a render function that receives the active item's value and variant. ```tsx ``` ### Item Detail Use `onItemClick` and `KanbanItemTitle` to open item details. `KanbanItemTitle` creates a clickable title zone that prevents drag-and-drop and triggers the callback instead. ```tsx {items.map((item) => (

{item.description}

))} ))} ``` ### Vertical Layout Set `orientation="vertical"` for a top-to-bottom column layout. ```tsx ``` ## Components | Component | Description | |-----------|-------------| | `Kanban` | Root provider. Wraps `DndContext` with collision detection, keyboard navigation, and screen reader announcements. | | `KanbanBoard` | Container for columns. Renders a `SortableContext` for column reordering. | | `KanbanColumn` | A sortable column container. Renders a `SortableContext` for its items. | | `KanbanColumnHandle` | Drag handle for reordering columns. Must be inside `KanbanColumn`. | | `KanbanItem` | A sortable item within a column. Supports cross-column drag. | | `KanbanItemHandle` | Drag handle for items. Must be inside `KanbanItem`. | | `KanbanItemTitle` | Clickable title zone inside `KanbanItem`. Prevents drag-and-drop and fires `onItemClick`. | | `KanbanOverlay` | Portal-rendered drag overlay preview. Accepts a render function for custom previews. | | `KanbanFinalZone` | Container for final drop columns. Only visible during item drag. | | `KanbanFinalColumn` | A droppable target inside `KanbanFinalZone` that triggers `onFinalDrop`. | ## API Reference ### Kanban The root component. Manages drag state, collision detection, and value changes. | Prop | Type | Default | Description | |------|------|---------|-------------| | `value` | `Record` | — | Column data. Keys are column IDs, values are item arrays. | | `onValueChange` | `(columns: Record) => void` | — | Called when items are moved between or within columns. | | `getItemValue` | `(item: T) => UniqueIdentifier` | — | Extracts a unique ID from each item. Required when `T` is an object. | | `onMove` | `(event: DragEndEvent & { activeIndex: number; overIndex: number }) => void` | — | Called on reorder instead of `onValueChange` when provided. Useful for server-side persistence. | | `onFinalDrop` | `(item: T, finalColumnId: UniqueIdentifier) => void` | — | Called when an item is dropped onto a `KanbanFinalColumn`. | | `onItemClick` | `(value: UniqueIdentifier) => void` | — | Called when a `KanbanItemTitle` is clicked. Receives the item's value. | | `finalColumns` | `UniqueIdentifier[]` | — | IDs of final drop columns (must match `KanbanFinalColumn` values). | | `strategy` | `SortableContextProps['strategy']` | `verticalListSortingStrategy` | Sorting strategy for items within columns. | | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Board layout direction. | | `flatCursor` | `boolean` | `false` | Use `cursor-default` instead of `cursor-grab` for all drag handles. | | `modifiers` | `DndContextProps['modifiers']` | — | dnd-kit modifiers for constraining drag movement. | | `accessibility` | `DndContextProps['accessibility']` | — | Custom accessibility announcements (merged with built-in announcements). | ### KanbanBoard Container for columns. Must be inside `Kanban`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `asChild` | `boolean` | `false` | Render as child element using `Slot`. | | `className` | `string` | — | Additional CSS classes. | ### KanbanColumn A sortable column. Must be inside `KanbanBoard` or `KanbanOverlay`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `value` | `UniqueIdentifier` | — | Unique column identifier. Must match a key in `Kanban`'s `value`. | | `asChild` | `boolean` | `false` | Render as child element using `Slot`. | | `asHandle` | `boolean` | `false` | Make the entire column a drag handle. | | `disabled` | `boolean` | `false` | Disable column dragging. | | `className` | `string` | — | Additional CSS classes. | ### KanbanColumnHandle Drag handle for columns. Must be inside `KanbanColumn`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `asChild` | `boolean` | `false` | Render as child element using `Slot`. | | `disabled` | `boolean` | — | Override column's disabled state. | | `className` | `string` | — | Additional CSS classes. | ### KanbanItem A sortable item. Must be inside `KanbanColumn` (within `KanbanBoard` or `KanbanOverlay`). | Prop | Type | Default | Description | |------|------|---------|-------------| | `value` | `UniqueIdentifier` | — | Unique item identifier. Must match the value returned by `getItemValue`. | | `asChild` | `boolean` | `false` | Render as child element using `Slot`. | | `asHandle` | `boolean` | `false` | Make the entire item a drag handle. | | `disabled` | `boolean` | `false` | Disable item dragging. | | `className` | `string` | — | Additional CSS classes. | ### KanbanItemHandle Drag handle for items. Must be inside `KanbanItem`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `asChild` | `boolean` | `false` | Render as child element using `Slot`. | | `disabled` | `boolean` | — | Override item's disabled state. | | `className` | `string` | — | Additional CSS classes. | ### KanbanItemTitle Clickable title zone inside `KanbanItem`. Intercepts pointer events to prevent drag-and-drop activation, and fires the root `onItemClick` callback on click. Renders a `