# Kanban Application 01 - Simple Task Board

> Manage tasks across To Do, In Progress, and Done columns with a drag-and-drop Kanban board. Add and remove tasks inline, making it a great starting point for lightweight project trackers and personal task boards.

A shadcn/ui block from the [@shadcn-dashboard](https://shadcndashboard.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-dashboard/kanban-application-01).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-dashboard/kanban-application-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-dashboard/kanban-application-01-light-bc8caaff2696ad35.png
- Registry item: https://shadcndashboard.dev/r/kanban-application-01.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcndashboard.dev/r/kanban-application-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `lucide-react`
- Registry items: `card`, `input`, `button`

## Files

### `src/components/shadcn-dashboard/blocks/kanban-application-01/page.tsx`

```tsx
import KanbanApplication from "@/components/shadcn-dashboard/blocks/kanban-application-01";

export default function page() {
  return <KanbanApplication />;
}
```

### `src/components/shadcn-dashboard/blocks/kanban-application-01/index.tsx`

```tsx
"use client";
import { useState } from "react";
import {
  DndContext,
  DragOverlay,
  PointerSensor,
  closestCorners,
  useSensor,
  useSensors,
  type DragStartEvent,
  type DragOverEvent,
  type DragEndEvent,
} from "@dnd-kit/core";
import { arrayMove } from "@dnd-kit/sortable";
import { Card } from "@/components/ui/card";
import KanbanColumn from "@/components/shadcn-dashboard/blocks/kanban-application-01/kanban-column";
import { initialColumns } from "@/components/shadcn-dashboard/blocks/kanban-application-01/data";
import { Column, Task } from "@/components/shadcn-dashboard/blocks/kanban-application-01/types";

const KanbanApplication = () => {
  const [columns, setColumns] = useState<Column[]>(initialColumns);
  const [activeTask, setActiveTask] = useState<Task | null>(null);

  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } })
  );

  const findColumn = (id: string): Column | null => {
    const byId = columns.find((col) => col.id === id);
    if (byId) return byId;
    return columns.find((col) => col.tasks.some((task) => task.id === id)) ?? null;
  };

  const handleDragStart = (event: DragStartEvent) => {
    const column = findColumn(event.active.id as string);
    const task = column?.tasks.find((t) => t.id === event.active.id) ?? null;
    setActiveTask(task);
  };

  const handleDragOver = (event: DragOverEvent) => {
    const { active, over } = event;
    if (!over || active.id === over.id) return;

    const activeColumn = findColumn(active.id as string);
    const overColumn = findColumn(over.id as string);
    if (!activeColumn || !overColumn || activeColumn.id === overColumn.id) return;

    setColumns((prev) => {
      const activeItems = activeColumn.tasks;
      const overItems = overColumn.tasks;
      const activeIndex = activeItems.findIndex((t) => t.id === active.id);
      const activeTaskItem = activeItems[activeIndex];
      if (!activeTaskItem) return prev;

      const overIndex = overItems.findIndex((t) => t.id === over.id);
      const insertAt = overIndex >= 0 ? overIndex : overItems.length;

      return prev.map((col) => {
        if (col.id === activeColumn.id) {
          return { ...col, tasks: activeItems.filter((t) => t.id !== active.id) };
        }
        if (col.id === overColumn.id) {
          const newTasks = [...overItems];
          newTasks.splice(insertAt, 0, activeTaskItem);
          return { ...col, tasks: newTasks };
        }
        return col;
      });
    });
  };

  const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    setActiveTask(null);
    if (!over) return;

    const activeColumn = findColumn(active.id as string);
    const overColumn = findColumn(over.id as string);
    if (!activeColumn || !overColumn || activeColumn.id !== overColumn.id) return;

    const activeIndex = activeColumn.tasks.findIndex((t) => t.id === active.id);
    const overIndex = overColumn.tasks.findIndex((t) => t.id === over.id);
    if (overIndex === -1 || activeIndex === overIndex) return;

    setColumns((prev) =>
      prev.map((col) =>
        col.id === activeColumn.id
          ? { ...col, tasks: arrayMove(col.tasks, activeIndex, overIndex) }
          : col
      )
    );
  };

  const handleAddTask = (columnId: string, content: string) => {
    setColumns((prev) =>
      prev.map((col) =>
        col.id === columnId
          ? {
              ...col,
              tasks: [
                ...col.tasks,
                { id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, content },
              ],
            }
          : col
      )
    );
  };

  const handleDeleteTask = (columnId: string, taskId: string) => {
    setColumns((prev) =>
      prev.map((col) =>
        col.id === columnId
          ? { ...col, tasks: col.tasks.filter((t) => t.id !== taskId) }
          : col
      )
    );
  };

  return (
    <div className="w-full ring-0 px-4 py-12 rounded-none overflow-x-auto max-w-7xl mx-auto">
      <DndContext
        sensors={sensors}
        collisionDetection={closestCorners}
        onDragStart={handleDragStart}
        onDragOver={handleDragOver}
        onDragEnd={handleDragEnd}
      >
        <div className="flex gap-4 items-start w-full min-w-3xl">
          {columns.map((column) => (
            <KanbanColumn
              key={column.id}
              column={column}
              onAddTask={handleAddTask}
              onDeleteTask={handleDeleteTask}
            />
          ))}
        </div>
        <DragOverlay>
          {activeTask ? (
            <div className="bg-card flex items-start gap-2 p-3 shadow-md w-72 rotate-2">
              <p className="text-sm text-card-foreground wrap-break-word">
                {activeTask.content}
              </p>
            </div>
          ) : null}
        </DragOverlay>
      </DndContext>
    </div>
  );
};

export default KanbanApplication;
```

### `src/components/shadcn-dashboard/blocks/kanban-application-01/kanban-column.tsx`

```tsx
"use client";
import { useState } from "react";
import { useDroppable } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { Plus } from "lucide-react";
import { Input } from "@/components/ui/input";
import TaskCard from "@/components/shadcn-dashboard/blocks/kanban-application-01/task-card";
import { Column } from "@/components/shadcn-dashboard/blocks/kanban-application-01/types";
import { Card } from "@/components/ui/card";

type Props = {
  column: Column;
  onAddTask: (columnId: string, content: string) => void;
  onDeleteTask: (columnId: string, taskId: string) => void;
};

const KanbanColumn = ({ column, onAddTask, onDeleteTask }: Props) => {
  const { setNodeRef } = useDroppable({ id: column.id });
  const [isAdding, setIsAdding] = useState(false);
  const [value, setValue] = useState("");
  const Icon = column.icon;

  const commitAdd = () => {
    if (value.trim()) {
      onAddTask(column.id, value.trim());
    }
    setValue("");
    setIsAdding(false);
  };

  return (
    <Card className="bg-muted flex flex-1 flex-col items-start min-w-0 p-2 border-none ring-0">
      <div className="flex flex-col gap-2 items-start shrink-0 w-full">
        <div className="flex items-center justify-center px-2 py-1.5 shrink-0 w-full">
          <div className="flex flex-1 gap-2 items-center min-w-0">
            <div className="flex gap-2 items-center shrink-0">
              <Icon size={16} className={column.iconClassName} />
              <p className="text-sm font-medium text-muted-foreground whitespace-nowrap">
                {column.title}
              </p>
            </div>
            <p className="text-xs text-muted-foreground whitespace-nowrap">
              {String(column.tasks.length).padStart(2, "0")}
            </p>
          </div>
          <button
            type="button"
            onClick={() => setIsAdding(true)}
            className="text-muted-foreground hover:text-foreground shrink-0"
          >
            <Plus size={16} />
          </button>
        </div>

        <div
          ref={setNodeRef}
          className="flex flex-col gap-2 items-start shrink-0 w-full min-h-2"
        >
          <SortableContext
            items={column.tasks.map((task) => task.id)}
            strategy={verticalListSortingStrategy}
          >
            {column.tasks.map((task) => (
              <TaskCard
                key={task.id}
                task={task}
                onDelete={() => onDeleteTask(column.id, task.id)}
              />
            ))}
          </SortableContext>

          {isAdding && (
            <Input
              autoFocus
              value={value}
              onChange={(e) => setValue(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") commitAdd();
                if (e.key === "Escape") {
                  setValue("");
                  setIsAdding(false);
                }
              }}
              onBlur={commitAdd}
              placeholder="Task title"
              className="bg-card text-sm shadow-sm"
            />
          )}
        </div>
      </div>
    </Card>
  );
};

export default KanbanColumn;
```

### `src/components/shadcn-dashboard/blocks/kanban-application-01/task-card.tsx`

```tsx
"use client";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Trash2 } from "lucide-react";
import { Task } from "@/components/shadcn-dashboard/blocks/kanban-application-01/types";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";

type Props = {
  task: Task;
  onDelete: () => void;
};

const TaskCard = ({ task, onDelete }: Props) => {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: task.id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  return (
    <Card
      ref={setNodeRef}
      style={style}
      {...attributes}
      {...listeners}
      className={cn(
        "group bg-card flex items-start justify-between border-none ring-0 py-3 gap-2 shadow-sm w-full cursor-grab active:cursor-grabbing touch-none",
        isDragging && "opacity-40"
      )}
    >
      <CardContent className="w-full flex items-center justify-between">
        <p className="text-sm text-card-foreground wrap-break-word">{task.content}</p>
        <Button
          variant="link"
          onPointerDown={(e) => e.stopPropagation()}
          onClick={onDelete}
          size="icon-xs"
          className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-red-500 shrink-0 transition-opacity"
        >
          <Trash2 size={14} className="size-4!" />
        </Button>
      </CardContent>
    </Card>
  );
};

export default TaskCard;
```

### `src/components/shadcn-dashboard/blocks/kanban-application-01/data.ts`

```ts
import { CircleDot, CircleCheck, CircleDashed } from "lucide-react";
import { Column } from "@/components/shadcn-dashboard/blocks/kanban-application-01/types";

export const initialColumns: Column[] = [
  {
    id: "todo",
    title: "To Do",
    icon: CircleDashed,
    iconClassName: "text-muted-foreground fill-muted-foreground",
    tasks: [
      { id: "task-1", content: "Configure Shadcn & Tailwind" },
      { id: "task-2", content: "Design drag-and-drop" },
      { id: "task-3", content: "Implement dark mode" },
    ],
  },
  {
    id: "in-progress",
    title: "In Progress",
    icon: CircleDot,
    iconClassName: "text-orange-400",
    tasks: [
      { id: "task-4", content: "Animate card drops" },
      { id: "task-5", content: "Sync state to local storage" },
    ],
  },
  {
    id: "done",
    title: "Done",
    icon: CircleCheck,
    iconClassName: "text-chart-2",
    tasks: [
      { id: "task-6", content: "Add Auth.js middleware" },
      { id: "task-7", content: "Scaffold sidebar layout" },
      { id: "task-8", content: "Init Next.js TS project" },
    ],
  },
];
```

### `src/components/shadcn-dashboard/blocks/kanban-application-01/types.ts`

```ts
import { LucideIcon } from "lucide-react";

export type Task = {
  id: string;
  content: string;
};

export type Column = {
  id: string;
  title: string;
  icon: LucideIcon;
  iconClassName: string;
  tasks: Task[];
};
```
