Kanban Board

DnD & Interaction

Kanban board with draggable columns and cards.

Preview

To Do2
Research competitors
Sketch wireframes
In Progress1
Build components
Done1
Set up repo

Usage

example.jsx
import { KanbanBoard } from "@/components/ui/kanban-board";

export default function Example() {
  return <KanbanBoard />;
}

Source Code

Copy this file into components/ui/kanban-board.jsx in your project.

kanban-board.jsx
"use client";

import { forwardRef, useState } from "react";
import { cn } from "@/lib/utils";

const KanbanBoard = forwardRef(({ className, columns: initial = [], ...props }, ref) => {
  const [columns, setColumns] = useState(initial);
  const [dragging, setDragging] = useState(null); // { colIdx, itemIdx }

  const onDragStart = (colIdx, itemIdx) => setDragging({ colIdx, itemIdx });

  const onDropOnColumn = (targetColIdx) => {
    if (!dragging || dragging.colIdx === targetColIdx) { setDragging(null); return; }
    const newCols = columns.map((col) => ({ ...col, items: [...col.items] }));
    const [item] = newCols[dragging.colIdx].items.splice(dragging.itemIdx, 1);
    newCols[targetColIdx].items.push(item);
    setColumns(newCols);
    setDragging(null);
  };

  return (
    <div ref={ref} className={cn("flex gap-4 overflow-x-auto p-4", className)} {...props}>
      {columns.map((col, ci) => (
        <div key={ci}
          onDragOver={(e) => e.preventDefault()}
          onDrop={() => onDropOnColumn(ci)}
          className="flex w-72 shrink-0 flex-col rounded-lg border bg-muted/30"
        >
          <div className="flex items-center justify-between border-b px-3 py-2">
            <span className="text-sm font-semibold">{col.title}</span>
            <span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{col.items.length}</span>
          </div>
          <div className="flex-1 space-y-2 p-3">
            {col.items.map((item, ii) => (
              <div key={ii} draggable
                onDragStart={() => onDragStart(ci, ii)}
                className="cursor-grab rounded-md border bg-background p-3 text-sm shadow-sm active:cursor-grabbing"
              >
                {typeof item === "string" ? item : item.title || JSON.stringify(item)}
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
});
KanbanBoard.displayName = "KanbanBoard";

export { KanbanBoard };

Quick Install

Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.

npm install clsx tailwind-merge