# Kanban

> A React kanban board for tasks, orders and pipelines: drag cards with mouse, touch or keyboard, WIP limits, inline add and a phone layout.

- Type: React component (Tables & Lists)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add kanban`
- Docs: https://wingo-ui.com/components/kanban

## Interaction

Drag a card with the mouse (it lifts after 8px, tilts 2 degrees and casts a floating shadow) to reorder it in its column or drop it in another; the cards below slide apart, a dashed outline marks where it lands, the column under it gets a ring and columns that refuse it dim. Near the board's edges the board scrolls sideways and a long column scrolls on its own. Every card is a Tab stop: Space picks it up, Up / Down move it in its column, Left / Right jump to the nearest spot in the neighbouring column, Space or Enter drops, Escape puts it back; Enter on a resting card opens it, and a screen reader hears where it is ("Northwind Logistics moved to Negotiation, position 2 of 4"). The ⋯ button on a card (shown on hover or focus, always on touch; Shift + F10 or the Menu key opens it too) has Move to, a submenu of columns, as the way to move without dragging. Column counts read 3 or 3 / 5 with a WIP limit: at the limit the count turns amber, over it red with a one-time pulse and a line of text; limitBehavior="block" refuses drops into a full column instead. + Add card opens a composer: Enter adds (and keeps it open for the next card), Shift + Enter makes a new line, Escape cancels. On a phone the board shows one column at a time with the next one peeking and snaps between them; the chip row on top jumps to a column and lights the one in view; a long press (250ms, with a haptic tick) lifts a card so swipes still scroll, and Move to opens as a bottom sheet. Reduced motion drops the tilt, the pulse and the sliding.

## Installation

```bash
npx wingo-ui@latest add kanban
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/kanban` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/kanban.tsx`

npm dependencies: `npm i @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))

## Usage

```tsx
import { useState } from "react"
import { Kanban, type KanbanColumnData, type KanbanItem } from "@/components/ui/kanban"

const columns: KanbanColumnData[] = [
  { id: "lead", title: "New lead", tone: "info" },
  { id: "offer", title: "Proposal sent", limit: 5 },
  { id: "won", title: "Won", tone: "success" },
]

export function Pipeline() {
  const [deals, setDeals] = useState<KanbanItem[]>([
    { id: "c02", columnId: "offer", title: "Northwind Logistics", amount: 48920, dueDate: "2026-09-26", tags: [{ label: "VIP", tone: "warning" }] },
    { id: "c05", columnId: "won", title: "Summit Builders", amount: 103400, assignees: [{ name: "Daniel Reed" }] },
  ])
  return (
    <div className="h-[560px]">
      <Kanban
        columns={columns}
        value={deals}
        onValueChange={(next) => setDeals(next)}
        locale="en-US"
        onAddCard={(columnId, title) => ({ id: crypto.randomUUID(), columnId, title })}
        labels={{ empty: "Drop deals here" }}
      />
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns?` | `KanbanColumnData[]` |  | The columns in order: { id, title, textValue?, tone?, color?, limit?, description?, total?, disabled?, collapsed? }. tone / color paint the header dot, limit is the WIP limit, total overrides the summed amounts (null hides it), disabled accepts no drops. Collapsed columns are not in v1. |
| `value?` | `KanbanItem[]` |  | The cards in order (controlled): { id, columnId, title, textValue?, description?, tags?, assignees?, dueDate?, priority?, amount?, cover?, meta?, disabled? }. A card whose columnId does not exist is not rendered (a dev warning names it). If you do not update value after onValueChange, the card springs back. |
| `defaultValue?` | `KanbanItem[]` |  | The starting cards while uncontrolled. |
| `onValueChange?` | `(items: KanbanItem[], move?: KanbanMoveEvent) => void` |  | The whole new array after a drop, a Move to or an added card; move is undefined for added cards. |
| `onMove?` | `(event: KanbanMoveEvent) => (void \| boolean)` |  | Runs before a move commits with { itemId, fromColumnId, toColumnId, fromIndex, toIndex }. Return false to cancel it and the card flies back. Dropping on the same spot fires nothing. |
| `renderCard?` | `(item: KanbanItem, state: { dragging: boolean; overlay: boolean }) => ReactNode` |  | Replaces the default card. The board still wraps it in the draggable, focusable surface and puts the ⋯ menu at its top end; overlay is true for the lifted copy. |
| `onCardClick?` | `(item: KanbanItem) => void` |  | A click, a tap or Enter on a card (not after a drag). Open the deal or task here. |
| `onAddCard?` | `(columnId: string, title: string) => (void \| KanbanItem \| Promise<unknown>)` |  | Shows + Add card under every column. Return the new card (or a promise of it) and the board inserts it at the end of the column; a promise shows Add loading and keeps the text if it rejects. |
| `getColumnActions?` | `(column: KanbanColumnData) => MenuEntry[]` |  | The column ⋯ menu, in the dropdown-menu MenuEntry shape (a bottom sheet on phones). |
| `getCardActions?` | `(item: KanbanItem) => MenuEntry[]` |  | The card ⋯ menu. Move to (a submenu of the other columns) is added in front unless readOnly. |
| `limitBehavior?` | `"warn" \| "block"` | `"warn"` | What a column limit does. warn: the count turns amber at the limit and red past it, with a pulse and a line of text. block: a full column refuses drops (it dims while you drag) and its Move to entry is disabled. |
| `showCounts?` | `boolean` | `true` | The card count next to each column title ("3 / 5" with a limit) and in the phone chips. |
| `showTotals?` | `boolean` | `true` | The sum of the cards' amounts in each column header, in currency. A column's own total wins. |
| `columnWidth?` | `number` | `288` | Column width in px on wide boards. Boards narrower than 576px show one column at 85% of the width with the next one peeking. |
| `size?` | `"sm" \| "md"` | `"md"` | Card padding and text: md 12px padding and 14px titles, sm 10px and 13px for dense overview boards. |
| `variant?` | `"tile" \| "plain"` | `"tile"` | Columns as flat gray tiles, or transparent with a hairline between them. |
| `cardVariant?` | `"surface" \| "outline"` | `"surface"` | Cards as raised white (dark: #1A1A1A) panels, or outlined with no fill. |
| `currency?` | `string` | `"USD"` | ISO 4217 code for card amounts and column totals ("$48,920" in en-US). |
| `mobileNav?` | `boolean` | `true` | The chip row above a phone board: one chip per column with its count, tap to jump, the column in view lights up. |
| `readOnly?` | `boolean` | `false` | No drag, no + Add card and no Move to; cards still open and custom card actions stay. |
| `loading?` | `boolean` | `false` | Three skeleton cards per column with the real card shape (three placeholder columns without columns). |
| `locale?` | `string` | `""` | Intl locale for counts, amounts and due dates. Empty uses the UIProvider locale, then the browser's. |
| `labels?` | `Partial<KanbanLabels>` |  | Every string, merged over DEFAULT_KANBAN_LABELS: region, nav, addCard, addCardPlaceholder, add, cancel, back, empty, emptyBoard, loading, moveTo, sheetTitle, cardActions ({item}), columnActions ({column}), cardCount(count), limit ({limit}), overLimit(count, limit), full, instructions, pickedUp / moved / dropped / cancelled ({item} {column} {n} {total}), movedTo, due / overdue ({date}), assignees ({names}), priority { low, medium, high, urgent }. |
| `classNames?` | `Partial<Record<"nav" \| "navItem" \| "board" \| "column" \| "header" \| "dot" \| "title" \| "count" \| "total" \| "menu" \| "description" \| "list" \| "card" \| "cardBody" \| "cover" \| "cardTitle" \| "cardDescription" \| "tags" \| "cardFooter" \| "cardMenu" \| "empty" \| "footer" \| "add" \| "composer" \| "overlay", string>>` |  | Extra classes for the inner parts. Full-bleed phone boards pass nav: "-mx-4 px-4" and board: "-mx-4 px-4 scroll-px-4". |
| `className?` | `string` |  | Extra classes for the root. The board fills its container's height; give the container one so columns scroll on their own. |

## Parts

### KanbanCard (component)

The default card: 16:9 cover, a two-line title, description, a meta line, tags, then due date (red once overdue), priority icon, amount and up to three assignee avatars. Use it inside renderCard to wrap or extend it; inside a board it reads the board's size, variant, currency and locale. Props: { item: KanbanItem; size?; variant?; currency?; locale?; labels?; overlay?; classNames? }.

### KanbanColumn (component)

One column on its own (a backlog in a side panel, a dashboard widget), with drag, add and menus. Props: KanbanProps & { column: KanbanColumnData }.

### DEFAULT_KANBAN_LABELS (constant)

The English strings, to spread under a translation. Type: KanbanLabels.

### KanbanColumnData (type)

One column: id, title, textValue, tone or color for the header dot, limit (the WIP limit), description, total (undefined sums the amounts, null hides it) and disabled (accepts no drops).

### KanbanItem (type)

One card: id, columnId, title, textValue, description, tags, assignees, dueDate (a Date or an ISO day), priority, amount (summed into the column total), cover (a 16:9 image url), meta and disabled (cannot be dragged).

### KanbanMoveEvent (type)

What onMove receives: itemId, fromColumnId, toColumnId, fromIndex and toIndex.

### KanbanPriority (type)

"low" | "medium" | "high" | "urgent".

## Examples

- **Task board with a WIP limit**: To do / In progress / In review / Done with priorities; "In progress" holds 3 and limitBehavior="block" refuses a fourth.
- **Orders with covers**: A shop's orders by status: 16:9 product covers, amounts in dollars and the carrier as meta.
- **Phone frame**: Below 576px of board width: snap columns with a peek, the chip nav, long-press drag and Move to.
- **Read-only overview**: readOnly, size="sm", variant="plain" and outlined cards: a dense board for a dashboard.
- **Add cards inline**: onAddCard returns a promise of the new card; Add shows a spinner for the fake 500ms save.
- **Loading skeleton**: Three skeleton cards per column in the real card shape while the board loads.
- **Custom cards**: renderCard draws support tickets; drag, keyboard and the ⋯ menu stay the board's.
- **KanbanCard on its own**: The default card outside a board, surface and outline.

## See also

- [Deal Pipeline](https://wingo-ui.com/components/deal-pipeline) ([Markdown](https://wingo-ui.com/components/deal-pipeline.md))
- [List](https://wingo-ui.com/components/list) ([Markdown](https://wingo-ui.com/components/list.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))

---

Canonical: https://wingo-ui.com/components/kanban
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
