# Kanban **Category**: react **URL**: https://ui.darkcode.dev/en/docs/react/components/kanban **Source**: https://raw.githubusercontent.com/DarkCode-Developers/darkcode-ui/refs/heads/main/apps/docs/content/docs/en/react/components/(data-display)/kanban.mdx > A drag-and-drop kanban board with columns, cards, and keyboard navigation for organizing tasks and workflows *** ## Import ```tsx import { Kanban, useKanban, useKanbanColumn, useKanbanCardPlaceholder } from '@darkcode-ui/react'; ``` ## Anatomy The board is composed from compound parts. State is managed by the `useKanban` hook, while each column wires its drag-and-drop behaviour with `useKanbanColumn`. ```tsx ``` ### Usage Cards can be dragged between columns with a pointer or the keyboard (focus a card, then activate its drag handle). The board keeps all items in a single list and filters them per column. ```tsx "use client"; import type {UseKanbanCardPlaceholderReturn, UseKanbanReturn} from "@darkcode-ui/react"; import type {CSSProperties} from "react"; import { Button, Chip, Kanban, useKanban, useKanbanCardPlaceholder, useKanbanColumn, } from "@darkcode-ui/react"; import {Ellipsis, Plus} from "@gravity-ui/icons"; type TagColor = "accent" | "default" | "success" | "warning" | "danger"; interface Task { id: string; title: string; status: string; tag: {label: string; color: TagColor}; } interface Column { id: string; title: string; color: string; } const columns: Column[] = [ {color: "var(--muted)", id: "todo", title: "To Do"}, {color: "var(--warning)", id: "in-progress", title: "In Progress"}, {color: "var(--success)", id: "done", title: "Done"}, ]; const initialTasks: Task[] = [ { id: "1", status: "todo", tag: {color: "accent", label: "Design"}, title: "Audit the onboarding flow", }, { id: "2", status: "todo", tag: {color: "warning", label: "Research"}, title: "Interview 5 power users", }, { id: "3", status: "in-progress", tag: {color: "accent", label: "Design"}, title: "New empty states", }, { id: "4", status: "in-progress", tag: {color: "success", label: "Eng"}, title: "Wire up drag and drop", }, {id: "5", status: "done", tag: {color: "success", label: "Eng"}, title: "Set up the board grid"}, ]; function KanbanColumn({ column, kanban, renderDropIndicator, }: { column: Column; kanban: UseKanbanReturn; renderDropIndicator: UseKanbanCardPlaceholderReturn["renderDropIndicator"]; }) { const {dragAndDropHooks, items} = useKanbanColumn(kanban, column.id, {renderDropIndicator}); return ( {column.title} {items.length} {(item: Task) => ( {item.tag.label} {item.title} )} ); } export function Default() { const kanban = useKanban({ getColumn: (task) => task.status, initialItems: initialTasks, setColumn: (task, status) => ({...task, status}), }); const {renderDropIndicator} = useKanbanCardPlaceholder({ renderIndicator: (target) => , }); return ( {columns.map((column) => ( ))} ); } ``` ### Notion Board ```tsx "use client"; import type {UseKanbanCardPlaceholderReturn, UseKanbanReturn} from "@darkcode-ui/react"; import type {CSSProperties} from "react"; import { Button, Chip, Kanban, useKanban, useKanbanCardPlaceholder, useKanbanColumn, } from "@darkcode-ui/react"; import {Ellipsis, Plus} from "@gravity-ui/icons"; type TagColor = "accent" | "default" | "success" | "warning" | "danger"; interface Task { id: string; title: string; status: string; tag: {label: string; color: TagColor}; } interface Column { id: string; title: string; color: string; } const columns: Column[] = [ {color: "var(--muted)", id: "not-started", title: "Not started"}, {color: "var(--accent)", id: "in-progress", title: "In progress"}, {color: "var(--warning)", id: "in-review", title: "In review"}, {color: "var(--success)", id: "done", title: "Done"}, ]; const initialTasks: Task[] = [ { id: "n1", status: "not-started", tag: {color: "accent", label: "Marketing"}, title: "Launch announcement post", }, { id: "n2", status: "not-started", tag: {color: "warning", label: "Ops"}, title: "Update vendor contracts", }, { id: "n3", status: "in-progress", tag: {color: "success", label: "Product"}, title: "Quarterly roadmap draft", }, { id: "n4", status: "in-review", tag: {color: "danger", label: "Legal"}, title: "Privacy policy revision", }, { id: "n5", status: "done", tag: {color: "default", label: "Docs"}, title: "Migrate the help center", }, ]; function KanbanColumn({ column, kanban, renderDropIndicator, }: { column: Column; kanban: UseKanbanReturn; renderDropIndicator: UseKanbanCardPlaceholderReturn["renderDropIndicator"]; }) { const {dragAndDropHooks, items} = useKanbanColumn(kanban, column.id, {renderDropIndicator}); return ( {column.title} {items.length} {(item: Task) => ( {item.title} {item.tag.label} )} ); } export function NotionBoard() { const kanban = useKanban({ getColumn: (task) => task.status, initialItems: initialTasks, setColumn: (task, status) => ({...task, status}), }); const {renderDropIndicator} = useKanbanCardPlaceholder({ renderIndicator: (target) => , }); return ( {columns.map((column) => ( ))} ); } ``` ### Project Board ```tsx "use client"; import type {UseKanbanCardPlaceholderReturn, UseKanbanReturn} from "@darkcode-ui/react"; import type {CSSProperties} from "react"; import { Avatar, Button, Chip, Kanban, useKanban, useKanbanCardPlaceholder, useKanbanColumn, } from "@darkcode-ui/react"; import {Calendar, Comment, Ellipsis, Plus} from "@gravity-ui/icons"; type Priority = "low" | "medium" | "high"; type PriorityColor = "default" | "warning" | "danger"; interface Task { id: string; title: string; status: string; priority: Priority; due: string; comments: number; assignees: {name: string; src?: string}[]; } interface Column { id: string; title: string; color: string; } const PRIORITY: Record = { high: {color: "danger", label: "High"}, low: {color: "default", label: "Low"}, medium: {color: "warning", label: "Medium"}, }; const columns: Column[] = [ {color: "var(--muted)", id: "backlog", title: "Backlog"}, {color: "var(--accent)", id: "todo", title: "To Do"}, {color: "var(--warning)", id: "in-progress", title: "In Progress"}, {color: "var(--success)", id: "done", title: "Done"}, ]; const initialTasks: Task[] = [ { assignees: [{name: "Ada Lovelace"}, {name: "Grace Hopper"}], comments: 3, due: "Jun 24", id: "p1", priority: "high", status: "backlog", title: "Define API contract for billing", }, { assignees: [{name: "Alan Turing"}], comments: 1, due: "Jun 26", id: "p2", priority: "medium", status: "todo", title: "Build invoice PDF export", }, { assignees: [{name: "Katherine Johnson"}, {name: "Edsger Dijkstra"}], comments: 8, due: "Jun 21", id: "p3", priority: "high", status: "in-progress", title: "Migrate auth to the new gateway", }, { assignees: [{name: "Barbara Liskov"}], comments: 0, due: "Jun 19", id: "p4", priority: "low", status: "done", title: "Add usage analytics events", }, ]; function Assignees({people}: {people: Task["assignees"]}) { return (
{people.map((person) => ( {person.src ? : null} {person.name .split(" ") .map((part) => part[0]) .join("")} ))}
); } function KanbanColumn({ column, kanban, renderDropIndicator, }: { column: Column; kanban: UseKanbanReturn; renderDropIndicator: UseKanbanCardPlaceholderReturn["renderDropIndicator"]; }) { const {dragAndDropHooks, items} = useKanbanColumn(kanban, column.id, {renderDropIndicator}); return ( {column.title} {items.length} {(item: Task) => (
{PRIORITY[item.priority].label} {item.due}
{item.title}
{item.comments}
)}
); } export function ProjectBoard() { const kanban = useKanban({ getColumn: (task) => task.status, initialItems: initialTasks, setColumn: (task, status) => ({...task, status}), }); const {renderDropIndicator} = useKanbanCardPlaceholder({ renderIndicator: (target) => , }); return ( {columns.map((column) => ( ))} ); } ``` ### Sizes The `size` prop scales column width, card padding, and font size. Use `sm`, `md` (default), or `lg`. ```tsx "use client"; import type {UseKanbanCardPlaceholderReturn, UseKanbanReturn} from "@darkcode-ui/react"; import type {CSSProperties} from "react"; import { Chip, Kanban, useKanban, useKanbanCardPlaceholder, useKanbanColumn, } from "@darkcode-ui/react"; type TagColor = "accent" | "default" | "success" | "warning" | "danger"; interface Task { id: string; title: string; status: string; tag: {label: string; color: TagColor}; } interface Column { id: string; title: string; color: string; } const columns: Column[] = [ {color: "var(--muted)", id: "todo", title: "To Do"}, {color: "var(--warning)", id: "in-progress", title: "In Progress"}, {color: "var(--success)", id: "done", title: "Done"}, ]; const initialTasks: Task[] = [ {id: "1", status: "todo", tag: {color: "accent", label: "Design"}, title: "Audit onboarding"}, {id: "2", status: "in-progress", tag: {color: "success", label: "Eng"}, title: "Drag and drop"}, {id: "3", status: "done", tag: {color: "success", label: "Eng"}, title: "Board grid"}, ]; function KanbanColumn({ column, kanban, renderDropIndicator, }: { column: Column; kanban: UseKanbanReturn; renderDropIndicator: UseKanbanCardPlaceholderReturn["renderDropIndicator"]; }) { const {dragAndDropHooks, items} = useKanbanColumn(kanban, column.id, {renderDropIndicator}); return ( {column.title} {items.length} {(item: Task) => ( {item.tag.label} {item.title} )} ); } function Board({size}: {size: "sm" | "md" | "lg"}) { const kanban = useKanban({ getColumn: (task) => task.status, initialItems: initialTasks, setColumn: (task, status) => ({...task, status}), }); const {renderDropIndicator} = useKanbanCardPlaceholder({ renderIndicator: (target) => , }); return ( {columns.map((column) => ( ))} ); } export function Sizes() { return (
{(["sm", "md", "lg"] as const).map((size) => (

{size}

))}
); } ``` ## Related Components - **List View**: Single-column interactive list with selection and item actions - **Table**: Structured data display in rows and columns - **DropZone**: Drag-and-drop file upload area with a file list preview ## Styling ### Passing Tailwind CSS classes Every part accepts a `className`, and the colored status dot reads the `--kanban-indicator-color` custom property: ```tsx Done ``` ### Customizing the component classes To customize the Kanban component classes, you can use the `@layer components` directive.
[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes). ```css @layer components { .kanban { --kanban-column-min-width: 320px; --kanban-column-gap: 24px; } .kanban__card-content { @apply shadow-none ring-1 ring-border; } } ``` DarkCode UI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize. ### CSS Classes The Kanban component uses these CSS classes ([View source styles](https://github.com/DarkCode-Developers/darkcode-ui/blob/main/packages/styles/components/kanban.css)): #### Base & Size Classes - `.kanban` - Root board container. CSS grid with horizontal auto-flow and snap scrolling. - `.kanban--sm` - Small size. Narrower columns (`240px`), tighter gap (`12px`). - `.kanban--md` - Medium size (default). Standard columns (`280px`), default gap (`16px`). - `.kanban--lg` - Large size. Wider columns (`320px`), spacious gap (`20px`). #### Element Classes - `.kanban__column` - Individual column section. - `.kanban__column-body` - Visual container for the card list and footer actions. - `.kanban__column-header` - Header area with indicator, title, count, and actions. - `.kanban__column-actions` - Hover-reveal action buttons inside the header. - `.kanban__column-indicator` - Colored status dot (when empty) or icon container (with children). - `.kanban__column-title` - Column heading text (`text-sm font-semibold`). - `.kanban__column-count` - Item count badge (`text-xs text-muted`). - `.kanban__card` - Outer card wrapper. Handles focus ring, cursor, and drag opacity. - `.kanban__card--sm`, `.kanban__card--md`, `.kanban__card--lg` - Card size variants. - `.kanban__card-content` - Inner Motion wrapper carrying background, radius, shadow, and layout. - `.kanban__card-content--sm`, `.kanban__card-content--md`, `.kanban__card-content--lg` - Content size variants. - `.kanban__card-list` - Scrollable GridList container with vertical gap between cards. - `.kanban__scroll-shadow` - Optional ScrollShadow wrapper for constrained, scrollable columns. - `.kanban__drop-indicator` - Custom drop indicator with animated height via `--kanban-drop-height`. - `.kanban__drag-handle` - Grip button, screen-reader-only by default. Reveals on focus. - `.kanban__empty` - Empty state placeholder centered inside an empty column. #### Interactive States - `.kanban__card:hover .kanban__card-content` - applies `shadow-sm`. - `.kanban__card[data-selected="true"] .kanban__card-content` - applies `bg-accent-soft`. - `.kanban__card[data-dragging]` - reduced opacity. - `.kanban__card[data-focus-visible="true"]` - focus ring with `ring-2`. - `.kanban__column-header:hover .kanban__column-actions` - opacity transitions to 1. - `.kanban__drop-indicator[data-drop-target]` - border and animated height. #### CSS Variables - `--kanban-column-min-width` - Minimum column width (default: `280px`). - `--kanban-column-gap` - Gap between columns (default: `16px`). - `--kanban-column-height` - Column height reference (default: `480px`). - `--kanban-drop-height` - Drop indicator placeholder height. Set automatically by `useKanbanCardPlaceholder`. - `--kanban-indicator-color` - Color of the column status dot. ## API Reference ### Kanban The root board container. Wraps children in a horizontal `ScrollShadow`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size variant controlling card padding, font size, and column width. | Also supports all [ScrollShadow](/docs/react/components/scroll-shadow) props (except `size`). ### Kanban.CardList The scrollable card list backed by React Aria `GridList`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `selectionMode` | `"none" \| "single" \| "multiple"` | `"none"` | Selection behavior for cards. | | `renderEmptyState` | `() => ReactNode` | — | Custom empty state content when the column has no cards. | | `dragAndDropHooks` | `DragAndDropHooks` | — | The hooks returned by `useKanbanColumn`. | Also supports all React Aria [GridList](https://react-spectrum.adobe.com/react-aria/GridList.html) props. ### Kanban.Card An individual draggable card. Wraps content in a Motion `layout` animation. Backed by React Aria `GridListItem`. | Prop | Type | Default | Description | |------|------|---------|-------------| | `textValue` | `string` | — | Accessible text value for the card. | | `contentClassName` | `string` | — | Class names for the inner Motion content surface. | | `children` | `ReactNode \| ((renderProps) => ReactNode)` | — | Card content. May be a render function receiving GridListItem render props. | Also supports all React Aria [GridListItem](https://react-spectrum.adobe.com/react-aria/GridList.html#gridlistitem) props. ### Kanban.DropIndicator Visual drop indicator shown during drag operations. | Prop | Type | Default | Description | |------|------|---------|-------------| | `target` | `DropTarget` | — | The drop target provided by React Aria. | | `height` | `number` | — | Height of the placeholder in pixels. Typically the dragged card's height via `--kanban-drop-height`. | ### Kanban.DragHandle Grip button for keyboard-accessible dragging. Screen-reader-only by default; reveals on focus. Uses `slot="drag"`. Also supports all React Aria [Button](https://react-spectrum.adobe.com/react-aria/Button.html) props. ### Kanban.ScrollShadow Optional scroll wrapper for constrained column heights. Use with `max-h-[...]` to enable vertical scrolling within a column. Also supports all [ScrollShadow](/docs/react/components/scroll-shadow) props. ### Other Parts `Kanban.Column`, `Kanban.ColumnHeader`, `Kanban.ColumnBody`, `Kanban.ColumnActions`, `Kanban.ColumnIndicator`, `Kanban.ColumnTitle`, and `Kanban.ColumnCount` are styling wrappers that render `section`, `header`, `div`, `h3`, and `span` elements respectively and accept standard HTML props. ### useKanban Manages the shared list data for the entire board. ```tsx const kanban = useKanban({ initialItems: tasks, getColumn: (item) => item.status, setColumn: (item, column) => ({ ...item, status: column }), }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `initialItems` | `T[]` | — | Items to populate the board with. | | `getColumn` | `(item: T) => string` | — | Return the column identifier for a given item. | | `setColumn` | `(item: T, column: string) => T` | — | Return a copy of the item assigned to a new column. | | `dragType` | `string` | `"kanban-item-id"` | Custom drag type for transferring item keys between columns. | | `getKey` | `(item: T) => string \| number` | `item.id` | Derive a unique key from each item. | **Returns:** `{ list, addItem, removeItem, moveItem, updateItem, getColumn, setColumn, getKey, dragType }`. ### useKanbanColumn Provides filtered items and drag-and-drop hooks for a single column. ```tsx const { items, dragAndDropHooks } = useKanbanColumn(kanban, "in-progress", { renderDropIndicator, }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `kanban` | `UseKanbanReturn` | — | The kanban instance from `useKanban`. | | `column` | `string` | — | The column identifier to filter items by. | | `options.renderDropIndicator` | `(target) => ReactElement` | — | Override the default drop indicator rendering. | **Returns:** `{ items, dragAndDropHooks }` — pass `dragAndDropHooks` to `Kanban.CardList`. ### useKanbanCardPlaceholder Measures the dragged card's height and writes it as a CSS variable for card-sized drop indicators. ```tsx const { renderDropIndicator } = useKanbanCardPlaceholder({ renderIndicator: (target) => , }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `renderIndicator` | `(target: DropTarget) => ReactElement` | — | Render function that returns a custom `Kanban.DropIndicator`. | **Returns:** `{ renderDropIndicator }` — pass to `useKanbanColumn` options.