@mshafiqyajid/react-kanban
Pointer-event drag system that works with mouse, touch, and keyboard. Column reorder, smooth FLIP animations, card-shaped drop placeholder, multi-select with batch drag, built-in search, rich card metadata (assignees, due dates, tags, checklist progress, counts, covers), per-column accents, WIP limits, locked columns, and a fully accessible controlled or uncontrolled API.
Playground #
Backlog2
Refresh marketing site heroAdd a new gradient mesh and product cover.
Investigate SSO login bugSome Okta users see a redirect loop on Safari 17.
To Do2
Ship dashboard tooltipsFinal pass on copy + a11y review.
Migrate billing portal to v3 SDK
In Progress1 / 3
New onboarding flowThree-step wizard with skip-for-now.
In Review1
Audit billing emails
Shipped1
Color tokens v2
Props
TSX
import { KanbanStyled } from "@mshafiqyajid/react-kanban/styled";
import "@mshafiqyajid/react-kanban/styles.css";
<KanbanStyled
columns={columns}
onChange={setColumns}
tone="primary"
searchable
selectable
reorderable
columnReorderable
animateLayout
showWipBadge
renameColumnInline
/>Install #
npm install @mshafiqyajid/react-kanban Quick start #
import { useState } from "react";
import { KanbanStyled } from "@mshafiqyajid/react-kanban/styled";
import type { KanbanColumn } from "@mshafiqyajid/react-kanban";
import "@mshafiqyajid/react-kanban/styles.css";
const initial: KanbanColumn[] = [
{ id: "todo", title: "To Do", cards: [], accent: "blue" },
{ id: "doing", title: "In Progress", cards: [], accent: "amber", wipLimit: 3 },
{ id: "done", title: "Done", cards: [], accent: "green" },
];
export default function Board() {
const [columns, setColumns] = useState<KanbanColumn[]>(initial);
return (
<KanbanStyled
columns={columns}
onChange={setColumns}
tone="primary"
searchable
selectable
columnReorderable
addCardPlaceholder="Add card"
addColumnPlaceholder="Add column"
/>
);
} Why this kanban #
- Touch + keyboard from day one. Pointer events under the hood, so cards move on phones, trackpads, and screen readers.
- Smooth, not snappy. FLIP animations slide cards into place when columns reflow.
- Real placeholder. A card-shaped drop slot opens at the target index β not a 1px line.
- Auto-scroll on edges. Scroll the board horizontally and a column vertically while dragging.
- Multi-select & batch drag. Shift- or Cmd-click to grab a stack and move it together.
- Search built in. Toggle
searchablefor a board-level filter; or passfilterto drive it yourself. - Rich card metadata. Assignees, due dates, tags, checklist progress, attachments / comments counts, covers, priority β all rendered by default.
- Accessible. Space picks up, arrows navigate, Enter drops, Esc cancels.
- Controlled or uncontrolled. Pass
columns+onChange, ordefaultColumnsfor self-managed state.
Rich cards #
const cards = [
{
id: "k1",
content: "Migrate billing portal",
description: "Move to v3 SDK before launch.",
priority: "urgent",
tags: ["billing", "infra"],
dueDate: "2026-06-01",
assignees: [
{ id: "u1", name: "Maya Linn", color: "#a78bfa" },
{ id: "u2", name: "Sam Vega", color: "#f97316" },
],
checklist: { done: 3, total: 5 },
attachments: 2,
comments: 7,
cover: "linear-gradient(120deg, #6366f1, #ec4899)",
},
]; Keyboard drag-and-drop #
- Tab to focus a card.
- Space to pick it up. β/β reorder; β/β move across columns.
- Enter or Space drops; Esc cancels.
- Same flow for column reorder when
columnReorderableis on (focus the column header).
Headless #
import { useKanban } from "@mshafiqyajid/react-kanban";
const {
columns,
drag,
selection,
getBoardProps,
getCardProps,
getColumnDropProps,
getColumnHandleProps,
addCard,
removeCard,
moveCard,
reorderColumn,
} = useKanban({
defaultColumns: [
{ id: "todo", title: "To Do", cards: [{ id: "1", content: "Ship docs" }] },
{ id: "done", title: "Done", cards: [] },
],
reorderable: true,
columnReorderable: true,
});
return (
<div {...getBoardProps()} className="board">
{columns.map((col) => (
<div key={col.id} {...getColumnDropProps(col.id)} className="col">
<header {...getColumnHandleProps(col.id)}>{col.title}</header>
{col.cards.map((card) => (
<div key={card.id} {...getCardProps(card.id, col.id)}>
{card.content}
</div>
))}
</div>
))}
</div>
); Search & filter #
// Built-in board-level search bar
<KanbanStyled columns={columns} onChange={setColumns} searchable searchPlaceholder="Search cardsβ¦" />
// Or supply your own predicate
<KanbanStyled
columns={columns}
onChange={setColumns}
filter={(card) => card.priority === "urgent" || card.priority === "high"}
/> Per-column accent + locked columns #
const columns: KanbanColumn[] = [
{ id: "todo", title: "To Do", cards: [], accent: "blue" },
{ id: "doing", title: "In Progress", cards: [], accent: "amber", wipLimit: 3 },
{ id: "block", title: "Blocked", cards: [], accent: "red", locked: true },
{ id: "done", title: "Shipped", cards: [], accent: "green" },
]; Accents are "blue" | "green" | "amber" | "red" | "violet" | "pink" | "cyan" | "neutral". locked: true rejects drops with reason "locked".
API #
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | KanbanColumn[] | β | Controlled columns + cards. |
| defaultColumns | KanbanColumn[] | β | Uncontrolled initial value (alternative to columns). |
| onChange | (cols) => void | β | Fires on every state change. |
| size | "sm" | "md" | "lg" | "md" | Card & text size. |
| tone | "neutral" | "primary" | "success" | "warning" | "danger" | "neutral" | Accent palette for highlights, focus rings, drop indicators. |
| columnMinWidth | string | "260px" | Minimum column width. |
| maxColumns | number | β | Cap visible columns. |
| reorderable | boolean | true | Allow intra-column drag-to-reorder. |
| columnReorderable | boolean | false | Drag column headers (or focus + Space) to reorder columns. |
| animateLayout | boolean | true | FLIP animation when cards reflow. Disable for huge boards. |
| showDropIndicator | boolean | true | Render the card-shaped drop placeholder. |
| searchable | boolean | false | Render a board-level search input. |
| searchPlaceholder | string | "Search cardsβ¦" | Search input placeholder. |
| filter | (card, col) => boolean | β | Custom predicate; return false to hide. |
| selectable | boolean | false | Enable Shift/Cmd-click multi-select with batch drag. |
| onSelectionChange | (ids) => void | β | Selection observer. |
| collapsible | boolean | false | Show a collapse chevron in the column header. |
| renameColumnInline | boolean | false | Double-click a title to edit; Enter commits, Esc cancels. |
| disabled | boolean | false | Disable drag, add, remove, rename. |
| addCardPlaceholder | string | β | Label for the "Add card" button (also enables it). |
| addCardPosition | "top" | "bottom" | "bottom" | Where new cards land. |
| addColumnPlaceholder | string | β | Label for the "Add column" tile (also enables it). |
| cardActions | { id, label, icon?, onAction, show? }[] | β | Hover-revealed buttons on each card. |
| showCardRemoveButton | boolean | false | Show a Γ on cards. |
| showWipBadge | boolean | false | "count / limit" badge when wipLimit is set. |
| maxCardsPerColumn | number | β | Global per-column cap (overridden by column.wipLimit). |
| cardDraggable | boolean | (card, col) => boolean | true | Per-card drag gating. |
| renderCard | (card, col) => ReactNode | β | Replace the default card body. |
| renderCardMeta | (card, col) => ReactNode | β | Append extra chips into the card footer. |
| renderColumnHeader | (col) => ReactNode | β | Replace the default column header. |
| renderEmptyColumn | (col) => ReactNode | β | Replace the empty-state inside a column. |
| renderEmptyBoard | () => ReactNode | β | Render when there are zero columns. |
| onCardAdd | (card, columnId) => void | β | β |
| onCardRemove | (card, columnId) => void | β | β |
| onCardClick | (card, col) => void | β | Click handler that doesn't fire on drag. |
| onCardMove | (card, from, to, toIndex) => void | β | Cross-column move. |
| onCardReorder | (card, columnId, fromIndex, toIndex) => void | β | Intra-column reorder. |
| onColumnAdd | (column) => void | β | β |
| onColumnRename | (columnId, title) => void | β | β |
| onColumnRemove | (column) => void | β | When provided, shows Γ in the column header. |
| onColumnReorder | (columnId, fromIndex, toIndex) => void | β | β |
| canDrop | (card, from, to, toIndex) => boolean | β | Validate before drop. Return false to reject. |
| onDropRejected | (card, from, to, reason) => void | β | reason: "canDrop" | "limit" | "locked" |
Card type #
interface KanbanCard<TData = unknown> {
id: string;
content: string;
description?: string;
label?: string;
priority?: "low" | "medium" | "high" | "urgent";
assignees?: { id: string; name: string; initials?: string; color?: string; avatarUrl?: string }[];
dueDate?: string | Date | null;
tags?: string[];
checklist?: { done: number; total: number };
attachments?: number;
comments?: number;
cover?: string; // any CSS background
data?: TData;
} Migrating from 0.x #
- Drag is now pointer events, not HTML5 DnD.
getDragProps/getDropPropson the headless hook were replaced bygetCardProps,getColumnDropProps,getColumnHandleProps. reorderabledefaults totrue. Passreorderable={false}to lock intra-column order.- The headless hook now exposes
drag(typed state),selection,moveCard,reorderColumn,cancelDrag. - The styled component still takes
columns/onChangeexactly as before; new opt-in props add features without forcing changes.