Skip to content
WANCRAFT

B.128Cards & Containers

Kanban Board

Drag cards within and between columns with a lifted ghost, animated placeholder, WIP limits and full keyboard moves.

NewReactMotionTailwindadvanced

Overview

Kanban Board is a controlled board of columns and cards with drag and drop that works for pointers and keyboards alike. Pressing and dragging a card lifts a tilted ghost into the top layer while a dashed placeholder slides to where the card will land, and the surrounding cards reflow on springs; near a column's top or bottom, or the board's sides, the view auto-scrolls. On release the ghost settles into place. From the keyboard, focus a card and press Space to pick it up, use the arrow keys to move it up and down or across columns, Space to drop and Escape to put it back, with each step announced ("Moved Fix login to In progress, position 2 of 4"). Column headers show the card count against an optional WIP limit and flag columns that exceed it, and add / remove hooks keep everything flowing through a single onChange(columns).

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/kanban-board.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/kanban-board.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/kanban-board.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { KanbanBoard, type KanbanColumn } from "@/components/ui/kanban-board";
import { useState } from "react";

export function KanbanBoardExample() {
  const [columns, setColumns] = useState<KanbanColumn[]>([
    { id: "todo", title: "To do", wipLimit: 4, cards: [{ id: "c1", title: "Fix login", tag: "Auth" }] },
    { id: "doing", title: "In progress", wipLimit: 2, cards: [{ id: "c2", title: "Billing page" }] },
    { id: "done", title: "Done", cards: [] },
  ]);

  return (
    <KanbanBoard
      columns={columns}
      onChange={setColumns}
      onAddCard={() => ({ id: crypto.randomUUID(), title: "New card" })}
    />
  );
}

03Props

PropTypeDefaultDescription
columnsKanbanColumn<C>[]—Required. { id, title, cards, wipLimit? }; cards are { id, title, description?, tag? } or your own type extending KanbanCard.
onChange(columns: KanbanColumn<C>[]) => void—Required. Receives the new columns after every move, add and remove (controlled).
renderCard(card: C, state: KanbanCardState) => ReactNode—Custom card body; state has dragging, grabbed, columnId and index. The draggable shell is provided.
onAddCard(columnId: string) => C | void—Shows an Add card button per column; return a card to append and focus it.
removablebooleanfalseShows a remove button on each card.
onRemoveCard(card: C, columnId: string) => void—Called after a card is removed.
showWipbooleantrueCount / limit badges and fill bars in column headers.
columnWidthnumber272Column width in px; the board scrolls sideways when columns overflow.
maxColumnHeightnumber420Card list height in px before a column scrolls.
autoScrollbooleantrueScroll columns and the board while dragging near their edges.
tiltnumber2Tilt of the lifted ghost in degrees.
spring{ stiffness; damping; mass }{ 520, 42, 1 }Spring for reflow and cross-column moves.
accentstring"#FF7448"Ghost border, placeholder, WIP fill and focus rings.
labelstring"Board"Accessible name of the board region.

04Features

  • 01Pointer drag within and between columns with a tilted top-layer ghost that settles into place on drop
  • 02Animated dashed placeholder; cards reflow and glide between columns with shared layout springs
  • 03Auto-scroll for tall columns and wide boards while dragging
  • 04Keyboard moves: Space to pick up, arrows within and across columns, Home / End, Space to drop, Escape to cancel
  • 05WIP limit badges and fill bars that turn red when a column is over its limit
  • 06Controlled onChange(columns) with add and remove hooks; custom card rendering
motion · springmotion · layoutinput · draginput · keyboardinput · pointerinput · click

05Accessibility

  • Each column is a labelled <section> with a <ul> of cards; cards are focusable role="button" elements with aria-roledescription="draggable card", aria-pressed while lifted and keyboard instructions via aria-describedby.
  • Every pick-up, move, drop, cancel, add and remove is announced in a polite live region, e.g. "Moved Fix login to In progress, position 2 of 4".
  • Focus follows a card when it moves to another column; moving focus away from a lifted card drops it in place.
  • WIP badges include screen-reader text for the card count and whether the column is over its limit.
  • On touch, only the grip starts a drag, so columns still scroll with a finger; remove buttons stay visible on devices without hover.
  • prefers-reduced-motion removes the tilt, the reflow springs and the settle animation.

06Limitations

  • Columns themselves cannot be reordered.
  • Large boards should virtualise long columns; every card is rendered.