# React Kanban Board: Drag, WIP Limits, Phone Layout

> Build a React kanban board that drags with mouse, touch and keyboard, enforces WIP limits, adds cards inline and shows one column at a time on a phone.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 16, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-kanban-board

## TL;DR

A production React kanban board needs mouse drag, a long press on touch, Space and arrow keys, and a Move to menu for people who cannot drag. It also needs WIP limits that warn or refuse drops, and a phone layout that shows one column at a time. The Wingo UI Kanban does this from a columns array and a controlled value; the Deal Pipeline adds stage probabilities and weighted totals on top.

You need a React kanban board for tasks, orders or a sales pipeline. Dragging cards between columns with a mouse is the easy part, and most tutorials stop there. Then someone opens it on a phone and the five columns shrink to slivers, a swipe drags a card when they meant to scroll, a keyboard user cannot move anything, and "In progress" quietly fills up with twelve cards. This guide covers what a production board needs, the props that matter in the Wingo UI [Kanban](https://wingo-ui.com/components/kanban), and a full example with saving, inline add and WIP limits.

## What should a React kanban board component handle?

Six things: three ways to drag, a way to move without dragging, WIP limits, inline add, a phone layout and screen reader announcements. Most React kanban board component demos only show the first one, a mouse on a wide screen. Here is the list we hold our own board to:

1. **Three ways to drag.** Mouse after 8px of movement (so a click still opens the card), touch after a 250ms long press (so a swipe still scrolls), keyboard with Space and the arrow keys.
2. **A way to move without dragging.** WCAG 2.2 success criterion [2.5.7 Dragging Movements](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) (level AA) asks for a single-pointer alternative to any drag unless dragging is essential. A Move to menu covers it.
3. **WIP limits** that warn or refuse the drop.
4. **Inline add** that stays open for the next card.
5. **A phone layout** with one column at a time.
6. **Announcements** of every move for screen readers.

Point 5 is the one demos skip: five 288px columns need 1,440px plus gaps, so on a 390px screen you either scroll sideways past slivers of columns or shrink cards until titles wrap after two words.

## How do the React kanban libraries compare?

Dice UI is a free source component you install through the shadcn CLI, SVAR is a packaged widget built for large boards, and the Wingo UI Kanban is a Pro source component with a phone layout and WIP limits built in. These are the three names that come up when you search for a React kanban library or a shadcn kanban. Facts are from each project's public docs (Dice UI's MIT license from its GitHub repository), as of October 2026.

| Option | What it is | Drag engine | Keyboard | Phone layout and WIP limits |
|---|---|---|---|---|
| [Dice UI Kanban](https://diceui.com/docs/components/base/kanban) | Free MIT source component, installs with `npx shadcn@latest add @diceui/kanban` | dnd kit | Enter or Space picks up, arrows move, Escape cancels | Not mentioned in the docs |
| [SVAR React Kanban](https://svar.dev/react/kanban/) | Packaged library, MIT core edition plus a paid PRO edition (export to PDF, PNG and Excel, undo and redo, dynamic column loading) | Built in, no extra drag library | Not mentioned on the product page | Not mentioned on the product page |
| [Wingo UI Kanban](https://wingo-ui.com/components/kanban) | Pro source component, installs with `npx wingo-ui@latest add kanban` | dnd kit | Space picks up, arrows move across columns, Space or Enter drops, Escape cancels | One column per screen below 576px, `limit` per column with warn or block |

Dice UI is a good choice if you want a free set of composable parts and plan to build the phone behavior yourself. SVAR fits if you want a configured widget: its page lists card and column virtualization, which matters once a board holds thousands of cards. The rest of this guide covers the Wingo UI board.

## What props does a React kanban board need?

Two arrays. `columns` lists the columns in order, and `value` (or `defaultValue`) lists the cards, each with a `columnId`. The board calls `onValueChange` with the whole new array after a drop, a Move to or an added card.

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

```tsx
"use client";

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" },
    { 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" />
    </div>
  );
}
```

The board fills its container's height, so give the wrapper one.

A card takes `title`, `description`, `tags`, `assignees`, `dueDate` (red once it is overdue), `priority`, `amount`, a 16:9 `cover` image and a muted `meta` line. Amounts are summed into the column header in `currency` (USD by default); set a column's `total` to override the sum or `null` to hide it. The props you will change most often after that are `limitBehavior`, `columnWidth` (288px by default), `size` (`sm` for dense overview boards) and `readOnly`.

Drag a card to another column, then tab to a card and press Space to move it with the arrow keys. Open a card's ⋯ menu to find Move to.

> Live demo (Kanban): Drag a deal into Negotiation and watch the column totals change. Tab to a card, press Space, move it with the arrow keys and press Space again to drop it, or use ⋯ then Move to. Try it at [Kanban](https://wingo-ui.com/components/kanban) and install it with `npx wingo-ui@latest add kanban`.

## How do kanban WIP limits work in the board?

A WIP (work in progress) limit is the most cards a column should hold at the same time. Kanban WIP limits exist to make a bottleneck visible: when "In review" is full, the team finishes reviews before starting new work. Jira's [column constraints](https://confluence.atlassian.com/jirasoftwareserver/configuring-columns-938845277.html) (documented for Jira Software Server) color a column red when it passes its maximum and yellow when it is under its minimum, as a visual signal only.

The Kanban supports both styles. Give a column a `limit` and its count reads "3 / 5":

- `limitBehavior="warn"` (the default): at the limit the count turns amber, over it red with a one-time pulse and a line of text ("Over the limit: 6 of 5"). The drop still goes through.
- `limitBehavior="block"`: a full column refuses drops, dims while you drag, and its entry in the Move to menu is disabled.

```tsx
{ id: "doing", title: "In progress", tone: "info", limit: 3, description: "At most 3 at a time" }
```

Use warn for teams that treat the limit as a conversation, and block for boards where the limit is a real capacity, such as a packing station that can only handle a fixed number of orders. A column with `disabled: true` accepts no drops at all, while its cards can still leave.

> Live demo (Kanban): Proposal sent has a limit of 5 and holds 4 deals. Drag one more in, then try a second: the full column dims and refuses it. Try it at [Kanban](https://wingo-ui.com/components/kanban) and install it with `npx wingo-ui@latest add kanban`.

## How do I save moves and add cards from a server?

Update state at once, send the change, and roll back if the request fails. `onValueChange` gets the new array and, for moves, a second argument: `{ itemId, fromColumnId, toColumnId, fromIndex, toIndex }`. `toIndex` is the position inside the target column, not in the flat `value` array, so send it with the column id and let the server renumber that column's sort keys. `onMove` runs before a move commits; return `false` to cancel it and the card flies back.

`onAddCard` 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. While the promise is pending the Add button shows a loading state, and if it rejects the text stays in the composer so nothing is lost. Enter adds and keeps the composer open for the next card, Shift + Enter makes a new line, and Escape closes it.

```tsx
"use client";

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

export function TaskBoard({ columns, initial }: { columns: KanbanColumnData[]; initial: KanbanItem[] }) {
  const [tasks, setTasks] = useState(initial);

  return (
    <div className="h-[calc(100dvh-8rem)]">
      <Kanban
        columns={columns}
        value={tasks}
        limitBehavior="block"
        // done cards cannot go back to the backlog
        onMove={(event) => !(event.fromColumnId === "done" && event.toColumnId === "todo")}
        onValueChange={(next, move) => {
          const previous = tasks;
          setTasks(next);
          if (!move) return;
          fetch(`/api/tasks/${move.itemId}`, {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ columnId: move.toColumnId, index: move.toIndex }),
          })
            .then((response) => {
              if (!response.ok) setTasks(previous);
            })
            .catch(() => setTasks(previous));
        }}
        onAddCard={async (columnId, title) => {
          const response = await fetch("/api/tasks", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ columnId, title }),
          });
          if (!response.ok) throw new Error("Not saved");
          return (await response.json()) as KanbanItem;
        }}
        locale="en-US"
      />
    </div>
  );
}
```

If you do not update `value` after `onValueChange`, the card springs back. That is the controlled contract, and it is why the rollback above is just a `setTasks(previous)`. The snapshot is only safe for one request at a time: if a second move lands before the first one fails, restoring the first snapshot also undoes the second. When moves can overlap, refetch the board on a failure instead.

## How do I make a kanban board work on mobile?

Show one column at a time. When the board is narrower than 576px, each column takes 85% of the width with the next one peeking from the edge, and the board snaps from column to column. A chip row on top shows every column with its count, lights the one in view, and jumps to a column on tap; `mobileNav={false}` hides it. The switch uses a container query on the board, so a narrow board in a side panel on a desktop gets the phone layout too.

Touch drag starts with a 250ms long press and a short haptic tick where the browser supports vibration, so a quick swipe scrolls between columns and a deliberate hold lifts the card. Hold a lifted card near the board's edge and the board scrolls sideways to the next column. The ⋯ button is always visible on touch, and Move to opens as a bottom sheet. The sensor setup is explained in the [dnd kit mobile touch drag tutorial](https://wingo-ui.com/blog/dnd-kit-mobile-touch-drag), and the sheet pattern in the [React bottom sheet guide](https://wingo-ui.com/blog/react-bottom-sheet-for-the-web).

For a board that runs to the screen edges, pass the full-bleed classes the docs suggest, so the peeking column starts at the edge of the screen:

```tsx
<Kanban columns={columns} value={deals} onValueChange={setDeals} classNames={{ nav: "-mx-4 px-4", board: "-mx-4 px-4 scroll-px-4" }} />
```

## How do I make a kanban board accessible?

Give every move a keyboard path, a menu path and a spoken announcement. Every card is a Tab stop. Space picks it up, Up and Down move it within its column, Left and Right jump to the nearest spot in the neighboring column, Space or Enter drops it, and Escape puts it back. Enter on a resting card runs `onCardClick`. Shift + F10 or the Menu key opens the card's ⋯ menu, which is built on the [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu); add your own entries with `getCardActions` and column entries with `getColumnActions`.

A screen reader hears each step from live announcements such as "Northwind Logistics moved to Negotiation, position 2 of 4". All strings, including those announcements, live in `labels` and merge over `DEFAULT_KANBAN_LABELS`, so you can translate the board without forking it. Give cards with a non-string `title` a `textValue` so menus and announcements have plain text to read. Reduced motion drops the tilt, the pulse and the sliding.

## What are the common React kanban board pitfalls?

Four mistakes show up once a board moves from demo data to real data.

- **No height on the container.** The board fills its parent. Without a height, columns grow with their cards and the page scrolls instead of the column.
- **A card with an unknown columnId.** It is not rendered, and a development warning names it. Check this when column ids come from a database.
- **Dragging as the only way to move.** Keep Move to in the menu (it is there unless `readOnly`), or the board no longer meets WCAG 2.5.7.
- **A board for a long list.** A column with hundreds of cards reads better as a table with a status filter, which turns into cards on phones ([responsive table pattern](https://wingo-ui.com/blog/responsive-table-mobile-cards)).

## When should I use the Deal Pipeline instead?

Use it when the board is a sales pipeline. The [Deal Pipeline](https://wingo-ui.com/components/deal-pipeline) is built on the Kanban and adds stage probabilities, total and weighted total per stage, a forecast summary with the win rate, and a reason dialog when a deal is dropped on Lost. `onDealMove` can return a promise: the card shows a spinner while it saves and springs back if the save fails. It also has a list view and an owner filter.

> Live demo (Deal Pipeline): Drag a deal to Lost: a dialog asks why before the move commits (on a phone, use ⋯ then Mark as lost). Switch to List in the toolbar to see the same deals grouped by stage. Try it at [Deal Pipeline](https://wingo-ui.com/components/deal-pipeline) and install it with `npx wingo-ui@latest add deal-pipeline`.

## How do I add it to my project?

Run `npx wingo-ui@latest add kanban` (or `deal-pipeline`). The CLI copies the source into `components/ui`, adds the components, hooks and helpers it imports, and installs `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `motion` and `lucide-react`. Both are Pro components ([pricing](https://wingo-ui.com/pricing)). More single-component deep dives are in the [component guides category](https://wingo-ui.com/blog/category/components), and the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide) applies the same touch, keyboard and screen reader rules to every form control.

## Components in this post

- [Kanban](https://wingo-ui.com/components/kanban) (Pro): A React kanban board for tasks, orders and pipelines: drag cards with mouse, touch or keyboard, WIP limits, inline add and a phone layout. Install: `npx wingo-ui@latest add kanban`
- [Deal Pipeline](https://wingo-ui.com/components/deal-pipeline) (Pro): A React sales pipeline board: deal stages with weighted totals, a forecast summary, drag between stages with a lost reason, a list view. Install: `npx wingo-ui@latest add deal-pipeline`

## FAQ

### What is the best React kanban library?

It depends on what you need around the board. A source component built on dnd kit, such as the Dice UI or Wingo UI kanban, gives you code you can edit; a packaged library such as SVAR React Kanban gives you a configured widget. Check touch drag, keyboard moves and a phone layout before you pick, because many boards only demo on a desktop.

### How do WIP limits work on a kanban board?

A WIP limit is the most cards a column should hold at once. Tools either warn (Jira colors a column red when it passes its maximum) or block the drop until a card leaves; the Wingo UI Kanban supports both through limitBehavior="warn" or "block".

### How do I make a kanban board work on mobile?

Show one column at a time with the next one peeking, snap between columns, and add a row of chips to jump to a column. Lift cards with a long press so a swipe still scrolls, and offer a Move to menu in a bottom sheet for people who do not want to drag.

### Is there a shadcn kanban component?

shadcn/ui itself does not ship a kanban board. Third-party registries do: Dice UI installs one through the shadcn CLI, and the Wingo UI Kanban installs as source with npx wingo-ui@latest add kanban.

### Is the Wingo UI Kanban free?

No. The Kanban and the Deal Pipeline are Pro components. The Empty State it uses for empty columns is free.

---

Source: https://wingo-ui.com/blog/react-kanban-board
