react ~14 KB 0 deps v1.0.0 β†— GitHub β†—

@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.design4ML
Investigate SSO login bugSome Okta users see a redirect loop on Safari 17.bugauth2AP
To Do2
Ship dashboard tooltipsFinal pass on copy + a11y review.docsTomorrow3/5MLKI
Migrate billing portal to v3 SDKbillinginfraYesterday112SV
In Progress1 / 3
New onboarding flowThree-step wizard with skip-for-now.growthToday4/67KIAP
In Review1
Audit billing emailsbillingcompliance2/21SV
Shipped1
Color tokens v2design-system8/8ML
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 searchable for a board-level filter; or pass filter to 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, or defaultColumns for 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 columnReorderable is 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 #

PropTypeDefaultDescription
columnsKanbanColumn[]β€”Controlled columns + cards.
defaultColumnsKanbanColumn[]β€”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.
columnMinWidthstring"260px"Minimum column width.
maxColumnsnumberβ€”Cap visible columns.
reorderablebooleantrueAllow intra-column drag-to-reorder.
columnReorderablebooleanfalseDrag column headers (or focus + Space) to reorder columns.
animateLayoutbooleantrueFLIP animation when cards reflow. Disable for huge boards.
showDropIndicatorbooleantrueRender the card-shaped drop placeholder.
searchablebooleanfalseRender a board-level search input.
searchPlaceholderstring"Search cards…"Search input placeholder.
filter(card, col) => booleanβ€”Custom predicate; return false to hide.
selectablebooleanfalseEnable Shift/Cmd-click multi-select with batch drag.
onSelectionChange(ids) => voidβ€”Selection observer.
collapsiblebooleanfalseShow a collapse chevron in the column header.
renameColumnInlinebooleanfalseDouble-click a title to edit; Enter commits, Esc cancels.
disabledbooleanfalseDisable drag, add, remove, rename.
addCardPlaceholderstringβ€”Label for the "Add card" button (also enables it).
addCardPosition"top" | "bottom""bottom"Where new cards land.
addColumnPlaceholderstringβ€”Label for the "Add column" tile (also enables it).
cardActions{ id, label, icon?, onAction, show? }[]β€”Hover-revealed buttons on each card.
showCardRemoveButtonbooleanfalseShow a Γ— on cards.
showWipBadgebooleanfalse"count / limit" badge when wipLimit is set.
maxCardsPerColumnnumberβ€”Global per-column cap (overridden by column.wipLimit).
cardDraggableboolean | (card, col) => booleantruePer-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 / getDropProps on the headless hook were replaced by getCardProps, getColumnDropProps, getColumnHandleProps.
  • reorderable defaults to true. Pass reorderable={false} to lock intra-column order.
  • The headless hook now exposes drag (typed state), selection, moveCard, reorderColumn, cancelDrag.
  • The styled component still takes columns/onChange exactly as before; new opt-in props add features without forcing changes.
Edit this page on GitHub