Arun Dass

Kanban Board

Cards 17

About

A fully interactive, multi-column Kanban board featuring native HTML5 drag and drop, inline task creation, rich card metadata, and a clean minimalist aesthetic.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Kanban Board to your project and fits it to your theme.

Code

How Kanban Board is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import React, { useState } from 'react';
import { MoreHorizontal, Calendar, GripVertical, CheckCircle2, MessageSquare, Paperclip, Plus, Trash2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';

// --- Types & Initial Data ---

type Priority = 'Low' | 'Medium' | 'High';

interface Tag {
  label: string;
  dotColor: string;
}

interface CardData {
  id: string;
  title: string;
  description?: string;
  tags?: Tag[];
  priority?: Priority;
  date?: string;
  avatars?: string[];
  tasksCompleted?: number;
  tasksTotal?: number;
  comments?: number;
  attachments?: number;
  coverImage?: string;
}

interface ColumnData {
  id: string;
  title: string;
  cards: CardData[];
}

const INITIAL_BOARD: ColumnData[] = [
  {
    id: 'col-1',
    title: 'To Do',
    cards: [
      {
        id: 'c-1',
        title: 'Design System Update',
        description: 'Audit existing components and create new variants for dark mode.',
        tags: [{ label: 'Design', dotColor: 'bg-purple-500' }],
        priority: 'Medium',
        date: 'Oct 15',
        comments: 3,
        attachments: 2,
        avatars: ["https://cdn.21st.dev/assets/mirror/42/420c64d5e90b2ff05fc182e3d8c3df40076440cb684d927efed247855775ea9b.jpg"]
      },
      {
        id: 'c-2',
        title: 'Landing Page Hero Iteration',
        coverImage: 'https://cdn.21st.dev/assets/mirror/cc/cc3a7096b528eb79324590d7ffc45687f5a3739012f41ac372cddfd306c7197c.jpg',
        tags: [{ label: 'Marketing', dotColor: 'bg-blue-500' }],
        priority: 'High',
        comments: 12,
        avatars: ["https://cdn.21st.dev/assets/mirror/ff/ff4b0fe2f7d11aff0d68f760e5fdcfeebf8b884c024ae24c9a6cf1d2edf76f49.jpg", "https://cdn.21st.dev/assets/mirror/9a/9a3f3f88dac2ceb807e98d4cbe99acc9813da6d0ce2859b1cf026747727e1667.jpg"]
      }
    ]
  },
  {
    id: 'col-2',
    title: 'In Progress',
    cards: [
      {
        id: 'c-3',
        title: 'Fix Mobile Navigation Bug',
        description: 'The hamburger menu doesn\'t close automatically when tapping outside the container.',
        tags: [{ label: 'Bug', dotColor: 'bg-orange-500' }],
        priority: 'High',
        date: 'Oct 12',
        tasksCompleted: 2,
        tasksTotal: 5,
        avatars: ["https://cdn.21st.dev/assets/mirror/75/75778ce28da1e1b2265c8ee1a8c261389266ee759a740e42f87de85012f3b301.jpg"]
      }
    ]
  },
  {
    id: 'col-3',
    title: 'Done',
    cards: [
      {
        id: 'c-4',
        title: 'Q3 Financial Report',
        tags: [{ label: 'Finance', dotColor: 'bg-green-500' }],
        priority: 'Low',
        date: 'Oct 01',
        attachments: 4,
        avatars: ["https://cdn.21st.dev/assets/mirror/b7/b78f8c42f62ae61f6ebe5c4e79f3af27f49dc05c4f366d2be599d1e14318be86.jpg"]
      }
    ]
  }
];

// --- Main Demo Component ---

export default function Demo() {
  const [board, setBoard] = useState<ColumnData[]>(INITIAL_BOARD);
  const [draggingCard, setDraggingCard] = useState<{ card: CardData; sourceColId: string } | null>(null);

  // --- Drag and Drop Handlers ---
  const handleDragStart = (e: React.DragEvent, card: CardData, colId: string) => {
    setDraggingCard({ card, sourceColId: colId });
    e.dataTransfer.effectAllowed = 'move';
    setTimeout(() => {
      if (e.target instanceof HTMLElement) e.target.classList.add('opacity-40');
    }, 0);
  };

  const handleDragEnd = (e: React.DragEvent) => {
    if (e.target instanceof HTMLElement) e.target.classList.remove('opacity-40');
    setDraggingCard(null);
  };

  const handleDrop = (e: React.DragEvent, targetColId: string) => {
    e.preventDefault();
    if (!draggingCard) return;
    if (draggingCard.sourceColId === targetColId) return;

    setBoard(prev => {
      const newBoard = [...prev];
      const sourceColIndex = newBoard.findIndex(c => c.id === draggingCard.sourceColId);
      const targetColIndex = newBoard.findIndex(c => c.id === targetColId);

      // Remove from source
      newBoard[sourceColIndex] = {
        ...newBoard[sourceColIndex],
        cards: newBoard[sourceColIndex].cards.filter(c => c.id !== draggingCard.card.id)
      };

      // Add to target
      newBoard[targetColIndex] = {
        ...newBoard[targetColIndex],
        cards: [...newBoard[targetColIndex].cards, draggingCard.card]
      };

      return newBoard;
    });
  };

  // --- Interactive Actions ---
  const handleAddCard = (colId: string, title: string) => {
    const newCard: CardData = {
      id: `c-${Date.now()}`,
      title,
      tags: [{ label: 'New', dotColor: 'bg-blue-500' }],
      date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
    };

    setBoard(prev => prev.map(col => {
      if (col.id === colId) {
        return { ...col, cards: [...col.cards, newCard] };
      }
      return col;
    }));
  };

  const handleDeleteCard = (colId: string, cardId: string) => {
    setBoard(prev => prev.map(col => {
      if (col.id === colId) {
        return { ...col, cards: col.cards.filter(c => c.id !== cardId) };
      }
      return col;
    }));
  };

  return (
    <div className="w-full min-h-screen bg-gray-50 dark:bg-[#111113] p-8 md:p-12 overflow-x-auto transition-colors duration-300 flex font-sans">
      <div className="flex items-start gap-6 mx-auto">
        {board.map((col) => (
          <KanbanColumn 
            key={col.id} 
            col={col} 
            onDragStart={handleDragStart}
            onDragEnd={handleDragEnd}
            onDrop={handleDrop}
            onAddCard={handleAddCard}
            onDeleteCard={handleDeleteCard}
          />
        ))}
      </div>
    </div>
  );
}

// --- Column Component ---

function KanbanColumn({ col, onDragStart, onDragEnd, onDrop, onAddCard, onDeleteCard }: any) {
  const [isAdding, setIsAdding] = useState(false);
  const [newTaskTitle, setNewTaskTitle] = useState("");

  const submitNewCard = () => {
    if (newTaskTitle.trim()) {
      onAddCard(col.id, newTaskTitle);
    }
    setNewTaskTitle("");
    setIsAdding(false);
  };

  return (
    <div 
      className="flex flex-col w-full min-w-[320px] max-w-[320px]"
      onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
      onDrop={(e) => onDrop(e, col.id)}
    >
      {/* Column Header */}
      <div className="flex items-center justify-between px-1 mb-4">
        <h3 className="text-sm font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
          {col.title}
          <span className="text-xs font-semibold bg-gray-200 dark:bg-[#25262b] text-gray-600 dark:text-gray-400 px-2 py-0.5 rounded-full">
            {col.cards.length}
          </span>
        </h3>
        <button className="text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
          <MoreHorizontal size={20} />
        </button>
      </div>

      {/* Cards List */}
      <div className="flex flex-col gap-4 min-h-[100px] rounded-2xl transition-colors">
        <AnimatePresence>
          {col.cards.map((card: CardData) => (
            <motion.div
              key={card.id}
              layout
              initial={{ opacity: 0, y: 10 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, scale: 0.95 }}
              draggable
              onDragStart={(e: any) => onDragStart(e, card, col.id)}
              onDragEnd={onDragEnd}
            >
              <KanbanCard card={card} onDelete={() => onDeleteCard(col.id, card.id)} />
            </motion.div>
          ))}
        </AnimatePresence>
        
        {/* Empty State Visual Hint */}
        {col.cards.length === 0 && !isAdding && (
          <div className="h-24 rounded-2xl border-2 border-dashed border-gray-200 dark:border-gray-800 flex items-center justify-center text-sm text-gray-400">
            Drop cards here
          </div>
        )}

        {/* Add Card Inline Input */}
        {isAdding ? (
          <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-2">
            <input 
              autoFocus
              type="text"
              placeholder="What needs to be done?"
              className="w-full p-3 text-sm rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-[#1a1b1e] text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-blue-500 shadow-sm transition-all"
              value={newTaskTitle}
              onChange={e => setNewTaskTitle(e.target.value)}
              onKeyDown={e => {
                if (e.key === 'Enter') submitNewCard();
                if (e.key === 'Escape') setIsAdding(false);
              }}
              onBlur={() => {
                if (newTaskTitle.trim()) submitNewCard();
                else setIsAdding(false);
              }}
            />
          </motion.div>
        ) : (
          <button 
            onClick={() => setIsAdding(true)}
            className="mt-2 flex items-center justify-center gap-2 w-full py-3 rounded-xl border-2 border-dashed border-gray-200 dark:border-gray-800/60 text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:hover:text-gray-300 dark:hover:border-gray-700 transition-colors text-sm font-semibold"
          >
            <Plus size={16} /> Add Task
          </button>
        )}
      </div>
    </div>
  );
}

// --- Card Component ---

function KanbanCard({ card, onDelete }: { card: CardData, onDelete: () => void }) {
  const [showMenu, setShowMenu] = useState(false);
  
  const getPriorityColor = (p?: Priority) => {
    switch(p) {
      case 'High': return 'text-red-600 bg-red-50 dark:bg-red-500/10 dark:text-red-400';
      case 'Medium': return 'text-orange-600 bg-orange-50 dark:bg-orange-500/10 dark:text-orange-400';
      case 'Low': return 'text-blue-600 bg-blue-50 dark:bg-blue-500/10 dark:text-blue-400';
      default: return 'text-gray-600 bg-gray-50 dark:bg-gray-800 dark:text-gray-400';
    }
  };

  return (
    <div
      className="group relative flex flex-col w-full bg-white dark:bg-[#1a1b1e] rounded-2xl border border-gray-200 dark:border-gray-800/80 shadow-sm hover:shadow-md transition-all duration-200 cursor-grab active:cursor-grabbing overflow-visible"
    >
      {/* Optional Cover Image */}
      {card.coverImage && (
        <div className="w-full h-32 overflow-hidden rounded-t-2xl border-b border-gray-100 dark:border-gray-800">
          <img src={card.coverImage} alt="Cover" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
        </div>
      )}

      <div className="flex flex-col gap-4 p-5">
        
        {/* Subtle Drag Handle on Hover */}
        <div className="absolute top-1/2 -left-3 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity text-gray-300 dark:text-gray-600">
          <GripVertical size={16} />
        </div>

        {/* Header: Tags, Priority & Action Menu */}
        <div className="flex items-center justify-between">
          <div className="flex flex-wrap gap-2">
            {card.tags?.map((tag, idx) => (
              <span 
                key={idx} 
                className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-[11px] font-semibold tracking-wide bg-gray-100 dark:bg-[#25262b] text-gray-700 dark:text-gray-300 transition-colors uppercase"
              >
                <span className={`w-1.5 h-1.5 rounded-full ${tag.dotColor}`} />
                {tag.label}
              </span>
            ))}
            {card.priority && (
              <span className={`px-2 py-1 rounded-md text-[11px] font-bold tracking-wide uppercase ${getPriorityColor(card.priority)}`}>
                {card.priority}
              </span>
            )}
          </div>
          
          {/* Card Actions Menu */}
          <div className="relative">
            <button 
              onClick={(e) => { e.stopPropagation(); setShowMenu(!showMenu); }}
              className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors p-1"
            >
              <MoreHorizontal size={18} />
            </button>
            
            {showMenu && (
              <>
                <div className="fixed inset-0 z-10" onClick={() => setShowMenu(false)} />
                <div className="absolute right-0 mt-1 w-32 bg-white dark:bg-[#25262b] border border-gray-100 dark:border-gray-800 rounded-lg shadow-xl overflow-hidden z-20 py-1">
                  <button 
                    onClick={() => { onDelete(); setShowMenu(false); }}
                    className="w-full text-left px-3 py-2 text-sm font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors flex items-center gap-2"
                  >
                    <Trash2 size={14} /> Delete
                  </button>
                </div>
              </>
            )}
          </div>
        </div>

        {/* Body: Title & Description */}
        <div className="flex flex-col gap-1.5 mt-1">
          <h4 className="text-[15px] font-bold text-gray-900 dark:text-gray-100 leading-snug">
            {card.title}
          </h4>
          {card.description && (
            <p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 leading-relaxed">
              {card.description}
            </p>
          )}
        </div>

        {/* Footer: Meta details & Avatars */}
        <div className="flex items-center justify-between mt-2 pt-4 border-t border-gray-100 dark:border-gray-800/60">
          <div className="flex items-center gap-3.5 text-xs font-medium text-gray-500 dark:text-gray-400">
            
            {card.date && (
              <div className="flex items-center gap-1.5">
                <Calendar size={14} className="text-gray-400" />
                <span>{card.date}</span>
              </div>
            )}

            {card.tasksTotal !== undefined && card.tasksCompleted !== undefined && (
              <div className="flex items-center gap-1.5">
                <CheckCircle2 size={14} className={card.tasksCompleted === card.tasksTotal ? "text-green-500" : "text-gray-400"} />
                <span>{card.tasksCompleted}/{card.tasksTotal}</span>
              </div>
            )}

            {card.comments !== undefined && card.comments > 0 && (
              <div className="flex items-center gap-1.5 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer transition-colors">
                <MessageSquare size={14} className="text-gray-400" />
                <span>{card.comments}</span>
              </div>
            )}

            {card.attachments !== undefined && card.attachments > 0 && (
              <div className="flex items-center gap-1.5">
                <Paperclip size={14} className="text-gray-400" />
                <span>{card.attachments}</span>
              </div>
            )}

          </div>

          {/* Overlapping Avatars */}
          {card.avatars && card.avatars.length > 0 && (
            <div className="flex items-center -space-x-2 shrink-0 ml-4">
              {card.avatars.map((url, idx) => (
                <img 
                  key={idx}
                  src={url} 
                  alt="Assignee" 
                  className="w-7 h-7 rounded-full border-2 border-white dark:border-[#1a1b1e] object-cover ring-1 ring-gray-100 dark:ring-gray-800"
                />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

More by Arun Dass

View all
  • Dashboard Sidebar

    Dashboard Sidebar

    1.7K
  • AI Planning

    AI Planning

    452
  • AI Agent Processing States

    AI Agent Processing States

    178
  • FileUpload

    FileUpload

    155
  • Neomorphism Metric Card

    Neomorphism Metric Card

    87
  • Focus Hero

    Focus Hero

    38
  • AI Prompt Input Box

    AI Prompt Input Box

    2
  • macOS Fluid Floating Dock

    macOS Fluid Floating Dock

More in Cards

View all
  • Stats cards with links

    Stats cards with links

    120
  • Course Design Cards

    Course Design Cards

    1.1K
  • Statistics Card 2

    Statistics Card 2

    258
  • Card

    Card

    55
  • Card Studio

    Card Studio

    9
  • Statistics Card 7

    Statistics Card 7

    288
  • The Item one

    The Item one

    3
  • Weather Card

    Weather Card

    52