SCALE — Build Lab
UI部品 · REACT COMPONENT

カンバンボード

CATEGORYUI部品 TYPEReact Component EFFORT240〜600分 DIFFICULTY
PRIMARY CODE
tsx
'use client';
import { useState } from 'react';

type Card = { id: string; title: string; [k: string]: any };
type Column = { id: string; title: string; cards: Card[] };

export function KanbanBoard({
  initial, onMove,
}: {
  initial: Column[];
  onMove?: (cardId: string, fromCol: string, toCol: string) => void;
}) {
  const [cols, setCols] = useState<Column[]>(initial);
  const [dragging, setDragging] = useState<{ cardId: string; fromCol: string } | null>(null);

  const handleDrop = (toCol: string) => {
    if (!dragging || dragging.fromCol === toCol) return;
    setCols((cs) => {
      const card = cs.find((c) => c.id === dragging.fromCol)?.cards.find((c) => c.id === dragging.cardId);
      if (!card) return cs;
      return cs.map((c) => {
        if (c.id === dragging.fromCol) return { ...c, cards: c.cards.filter((x) => x.id !== dragging.cardId) };
        if (c.id === toCol)            return { ...c, cards: [...c.cards, card] };
        return c;
      });
    });
    onMove?.(dragging.cardId, dragging.fromCol, toCol);
    setDragging(null);
  };

  return (
    <div className="flex gap-4 overflow-x-auto pb-4">
      {cols.map((col) => (
        <div key={col.id}
          className="bg-zinc-900/50 border border-zinc-800 rounded-2xl p-3 min-w-[280px] w-[280px]"
          onDragOver={(e) => e.preventDefault()}
          onDrop={() => handleDrop(col.id)}>
          <div className="flex items-center justify-between mb-3">
            <h3 className="text-sm font-semibold text-zinc-200">{col.title}</h3>
            <span className="text-xs text-zinc-500">{col.cards.length}</span>
          </div>
          <div className="space-y-2 min-h-[200px]">
            {col.cards.map((card) => (
              <div key={card.id}
                draggable
                onDragStart={() => setDragging({ cardId: card.id, fromCol: col.id })}
                className="bg-zinc-800 rounded-xl p-3 cursor-move hover:bg-zinc-700 transition-colors">
                <div className="text-sm text-zinc-100">{card.title}</div>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}
前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • タスク管理
  • 商談管理
  • 応募者管理

カンバンボード

:LiTarget: 用途

D&D対応のカンバンボードUI。列追加・並び替え可能。

:LiSparkle: 特徴

  • D&D並び替え
  • 列追加
  • カードカスタム
  • 検索

:LiCode: コード(コピペ用)

'use client';
import { useState } from 'react';

type Card = { id: string; title: string; [k: string]: any };
type Column = { id: string; title: string; cards: Card[] };

export function KanbanBoard({
  initial, onMove,
}: {
  initial: Column[];
  onMove?: (cardId: string, fromCol: string, toCol: string) => void;
}) {
  const [cols, setCols] = useState<Column[]>(initial);
  const [dragging, setDragging] = useState<{ cardId: string; fromCol: string } | null>(null);

  const handleDrop = (toCol: string) => {
    if (!dragging || dragging.fromCol === toCol) return;
    setCols((cs) => {
      const card = cs.find((c) => c.id === dragging.fromCol)?.cards.find((c) => c.id === dragging.cardId);
      if (!card) return cs;
      return cs.map((c) => {
        if (c.id === dragging.fromCol) return { ...c, cards: c.cards.filter((x) => x.id !== dragging.cardId) };
        if (c.id === toCol)            return { ...c, cards: [...c.cards, card] };
        return c;
      });
    });
    onMove?.(dragging.cardId, dragging.fromCol, toCol);
    setDragging(null);
  };

  return (
    <div className="flex gap-4 overflow-x-auto pb-4">
      {cols.map((col) => (
        <div key={col.id}
          className="bg-zinc-900/50 border border-zinc-800 rounded-2xl p-3 min-w-[280px] w-[280px]"
          onDragOver={(e) => e.preventDefault()}
          onDrop={() => handleDrop(col.id)}>
          <div className="flex items-center justify-between mb-3">
            <h3 className="text-sm font-semibold text-zinc-200">{col.title}</h3>
            <span className="text-xs text-zinc-500">{col.cards.length}</span>
          </div>
          <div className="space-y-2 min-h-[200px]">
            {col.cards.map((card) => (
              <div key={card.id}
                draggable
                onDragStart={() => setDragging({ cardId: card.id, fromCol: col.id })}
                className="bg-zinc-800 rounded-xl p-3 cursor-move hover:bg-zinc-700 transition-colors">
                <div className="text-sm text-zinc-100">{card.title}</div>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加