Skip to content
All Taste components

Productivity

Kanban board

Three columns of task cards with tags, assignees, due dates and comment counts.

3 columns · counts · tags · avatars · due date · comments · checklist progress · add card

To do3
Marketing
Write the Diwali offer email
17 Oct2
RS
Content
Shoot photos for the new menu
21 Oct
AKMN
FinanceUrgent
Update GST details on invoices
Today1
KR
In progress2
Design
Menu page redesign
19 Oct63/5
AMPN
Engineering
Payment reminder emails
23 Oct3
SK
Review2
Marketing
Pricing page copy
16 Oct4
RSAR
Engineering
Booking form validation
18 Oct14/4
SK
import { CalendarDays, CheckSquare, MessageSquare, Plus } from "lucide-react";
import { cn } from "@/lib/utils";

export interface Task {
  id: string;
  title: string;
  tags: string[];
  assignees: string[];
  due: string;
  overdue?: boolean;
  comments?: number;
  checklist?: [done: number, total: number];
}
export interface Column { id: string; title: string; tasks: Task[] }

const sample: Column[] = [
  { id: "todo", title: "To do", tasks: [
    { id: "t1", title: "Write the Diwali offer email", tags: ["Marketing"], assignees: ["Ritika Shah"], due: "17 Oct", comments: 2 },
    { id: "t2", title: "Shoot photos for the new menu", tags: ["Content"], assignees: ["Anil Kumar", "Maya Nair"], due: "21 Oct" },
    { id: "t3", title: "Update GST details on invoices", tags: ["Finance", "Urgent"], assignees: ["Kabir Rao"], due: "Today", overdue: true, comments: 1 },
  ] },
  { id: "doing", title: "In progress", tasks: [
    { id: "t4", title: "Menu page redesign", tags: ["Design"], assignees: ["Arjun Mehta", "Priya Nair"], due: "19 Oct", comments: 6, checklist: [3, 5] },
    { id: "t5", title: "Payment reminder emails", tags: ["Engineering"], assignees: ["Sana Khan"], due: "23 Oct", comments: 3 },
  ] },
  { id: "review", title: "Review", tasks: [
    { id: "t6", title: "Pricing page copy", tags: ["Marketing"], assignees: ["Ritika Shah", "Asha Raman"], due: "16 Oct", comments: 4 },
    { id: "t7", title: "Booking form validation", tags: ["Engineering"], assignees: ["Sana Khan"], due: "18 Oct", comments: 1, checklist: [4, 4] },
  ] },
];

const initials = (n: string) => n.split(" ").map((w) => w[0]).join("").slice(0, 2);

function TaskCard({ task }: { task: Task }) {
  return (
    <article className="grid gap-2.5 rounded border border-border bg-bg px-3 py-2.5 text-[13.5px] shadow-sm" tabIndex={0}>
      <div className="flex flex-wrap gap-1.5">
        {task.tags.map((t) => (
          <span key={t} className={cn("rounded-full px-[7px] py-px text-[11.5px] font-medium",
            t === "Urgent" ? "bg-danger-soft text-danger-fg" : "bg-surface-2 text-fg-muted")}>{t}</span>
        ))}
      </div>
      <p className="font-medium leading-snug">{task.title}</p>
      <div className="flex items-center justify-between gap-2 text-xs text-fg-subtle">
        <div className="flex items-center gap-2.5 [&_svg]:size-[13px]">
          <span className={cn("inline-flex items-center gap-1", task.overdue && "text-danger-fg")}><CalendarDays aria-hidden />{task.due}</span>
          {!!task.comments && <span className="inline-flex items-center gap-1"><MessageSquare aria-label="Comments" />{task.comments}</span>}
          {task.checklist && <span className="inline-flex items-center gap-1 tabular-nums"><CheckSquare aria-label="Checklist" />{task.checklist[0]}/{task.checklist[1]}</span>}
        </div>
        <div className="flex [&>*:not(:first-child)]:-ml-1.5">
          {task.assignees.map((a) => (
            <span key={a} title={a} className="grid size-[22px] place-items-center rounded-full border-[1.5px] border-bg bg-primary-soft text-[10px] font-medium text-primary-soft-fg">{initials(a)}</span>
          ))}
        </div>
      </div>
    </article>
  );
}

export function KanbanBoard({ columns = sample, onAdd }: { columns?: Column[]; onAdd?: (columnId: string) => void }) {
  return (
    <div className="w-full overflow-x-auto">
      <div className="grid grid-cols-[repeat(3,minmax(220px,1fr))] items-start gap-3">
        {columns.map((col) => (
          <section key={col.id} aria-label={col.title} className="grid gap-2 rounded-lg bg-surface p-2">
            <header className="flex items-center gap-2 px-1 pb-0.5 pt-1 text-[13px] font-medium">
              {col.title}
              <span className="rounded-full bg-surface-2 px-[7px] text-xs text-fg-muted">{col.tasks.length}</span>
              <button onClick={() => onAdd?.(col.id)} aria-label={"Add card to " + col.title}
                className="ml-auto grid size-7 place-items-center rounded text-fg-muted hover:bg-surface-2">
                <Plus className="size-4" />
              </button>
            </header>
            {col.tasks.map((t) => <TaskCard key={t.id} task={t} />)}
          </section>
        ))}
      </div>
    </div>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.