# Kanban board with frame columns

> Kanban board with frame columns

A shadcn/ui block from the [@reui](https://reui.io) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/reui/c-kanban-3).

- Page, with a live preview: https://shadcn.exchange/r/reui/c-kanban-3
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/reui/c-kanban-3-light-37de20f0922090ba.png
- Registry item: https://reui.io/r/new-york-v4/c-kanban-3.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://reui.io/r/new-york-v4/c-kanban-3.json
```

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

## Dependencies

- Registry items: `@reui/badge`, `@reui/frame`, `@reui/kanban`

## Files

### `c-kanban-3.tsx`

```tsx
"use client"

import { ComponentProps, useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
  Frame,
  FrameHeader,
  FramePanel,
  FrameTitle,
} from "@/components/reui/frame"
import {
  Kanban,
  KanbanBoard,
  KanbanColumn,
  KanbanColumnContent,
  KanbanItem,
  KanbanItemHandle,
  KanbanOverlay,
} from "@/components/reui/kanban"

import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"

interface Task {
  id: string
  title: string
  label: string
  labelVariant:
    | "primary-light"
    | "success-light"
    | "warning-light"
    | "destructive-light"
    | "info-light"
}

const COLUMNS: Record<string, { title: string; icon: React.ReactNode }> = {
  todo: {
    title: "To Do",
    icon: (
      <IconPlaceholder
        lucide="CircleIcon"
        tabler="IconCircle"
        hugeicons="CircleIcon"
        phosphor="CircleIcon"
        remixicon="RiCircleLine"
        className="size-4"
      />
    ),
  },
  doing: {
    title: "In Progress",
    icon: (
      <IconPlaceholder
        lucide="CircleDot"
        tabler="IconCircleDot"
        hugeicons="RefreshDotIcon"
        phosphor="DotsThreeCircleIcon"
        remixicon="RiRecordCircleLine"
        className="text-muted-foreground size-4"
      />
    ),
  },
  done: {
    title: "Done",
    icon: (
      <IconPlaceholder
        lucide="CircleCheckIcon"
        tabler="IconCircleCheck"
        hugeicons="CheckmarkCircle01Icon"
        phosphor="CheckCircleIcon"
        remixicon="RiCheckboxCircleLine"
        className="size-4"
      />
    ),
  },
}

function TaskCard({
  task,
  asHandle,
  ...props
}: { task: Task; asHandle?: boolean } & Omit<
  ComponentProps<typeof KanbanItem>,
  "value" | "children"
>) {
  const content = (
    <Frame variant="ghost" spacing="sm" className="p-0">
      <FramePanel className="p-3">
        <div className="flex flex-col gap-2">
          <span className="text-sm font-medium">{task.title}</span>
          <Badge variant={task.labelVariant} size="sm" className="w-fit">
            {task.label}
          </Badge>
        </div>
      </FramePanel>
    </Frame>
  )

  return (
    <KanbanItem value={task.id} {...props}>
      {asHandle ? <KanbanItemHandle>{content}</KanbanItemHandle> : content}
    </KanbanItem>
  )
}

export function Pattern() {
  const [columns, setColumns] = useState<Record<string, Task[]>>({
    todo: [
      {
        id: "1",
        title: "Design landing page",
        label: "Design",
        labelVariant: "info-light",
      },
      {
        id: "2",
        title: "Set up CI/CD pipeline",
        label: "DevOps",
        labelVariant: "warning-light",
      },
      {
        id: "3",
        title: "Write unit tests",
        label: "Testing",
        labelVariant: "success-light",
      },
    ],
    doing: [
      {
        id: "4",
        title: "Implement auth flow",
        label: "Backend",
        labelVariant: "primary-light",
      },
      {
        id: "5",
        title: "Create component library",
        label: "Frontend",
        labelVariant: "destructive-light",
      },
    ],
    done: [
      {
        id: "6",
        title: "Project kickoff",
        label: "Planning",
        labelVariant: "info-light",
      },
    ],
  })

  return (
    <Kanban
      value={columns}
      onValueChange={setColumns}
      getItemValue={(item) => item.id}
    >
      <KanbanBoard className="grid auto-rows-fr grid-cols-3">
        {Object.entries(columns).map(([columnId, tasks]) => {
          const col = COLUMNS[columnId] ?? { title: columnId, icon: null }
          return (
            <KanbanColumn key={columnId} value={columnId}>
              <Frame spacing="sm" className="h-full">
                <FrameHeader className="flex flex-row items-center gap-2">
                  {col.icon}
                  <FrameTitle>{col.title}</FrameTitle>
                  <Badge variant="outline" size="sm" className="ml-auto">
                    {tasks.length}
                  </Badge>
                </FrameHeader>
                <KanbanColumnContent
                  value={columnId}
                  className="flex flex-col gap-2 p-0.5"
                >
                  {tasks.map((task) => (
                    <TaskCard key={task.id} task={task} asHandle />
                  ))}
                </KanbanColumnContent>
              </Frame>
            </KanbanColumn>
          )
        })}
      </KanbanBoard>
      <KanbanOverlay className="bg-muted/10 rounded-md border-2 border-dashed" />
    </Kanban>
  )
}
```
