# Minimal kanban with stacked frame

> Minimal kanban with stacked frame

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

- Page, with a live preview: https://shadcn.exchange/r/reui/c-kanban-4
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/reui/c-kanban-4-light-2e0edded12660073.png
- Registry item: https://reui.io/r/new-york-v4/c-kanban-4.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-4.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: `avatar`, `@reui/badge`, `@reui/frame`, `@reui/kanban`, `progress`

## Files

### `c-kanban-4.tsx`

```tsx
"use client"

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

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"
import { Progress } from "@/components/ui/progress"

interface Task {
  id: string
  title: string
  assignee: string
  assigneeAvatar: string
  progress: number
}

function TaskCard({
  task,
  asHandle,
  ...props
}: { task: Task; asHandle?: boolean } & Omit<
  ComponentProps<typeof KanbanItem>,
  "value" | "children"
>) {
  const content = (
    <Frame variant="ghost" spacing="xs" className="p-0">
      <FramePanel className="space-y-3 p-3">
        <p className="text-sm font-medium">{task.title}</p>
        <Progress
          value={task.progress}
          className="[&_[data-slot=progress-track]]:h-1"
        />
        <div className="flex items-center justify-between">
          <div className="flex items-center gap-1.5">
            <Avatar className="size-5">
              <AvatarImage src={task.assigneeAvatar} alt={task.assignee} />
              <AvatarFallback className="text-[9px]">
                {task.assignee.charAt(0)}
              </AvatarFallback>
            </Avatar>
            <span className="text-muted-foreground text-xs">
              {task.assignee}
            </span>
          </div>
          <span className="text-muted-foreground text-xs">
            {task.progress}%
          </span>
        </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[]>>({
    planning: [
      {
        id: "1",
        title: "Research competitors",
        assignee: "Alex J.",
        assigneeAvatar:
          "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
        progress: 20,
      },
    ],
    active: [
      {
        id: "2",
        title: "Build dashboard",
        assignee: "Sarah C.",
        assigneeAvatar:
          "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
        progress: 65,
      },
      {
        id: "3",
        title: "API integration",
        assignee: "David K.",
        assigneeAvatar:
          "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
        progress: 40,
      },
    ],
    completed: [
      {
        id: "4",
        title: "Setup repository",
        assignee: "Emma W.",
        assigneeAvatar:
          "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
        progress: 100,
      },
    ],
  })

  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]) => (
          <KanbanColumn key={columnId} value={columnId}>
            <Frame stacked spacing="sm" className="h-full">
              <FrameHeader>
                <div className="flex items-center justify-between">
                  <FrameTitle className="capitalize">{columnId}</FrameTitle>
                  <Badge variant="outline" size="sm">
                    {tasks.length}
                  </Badge>
                </div>
                <FrameDescription>
                  {columnId === "planning" && "Tasks being scoped"}
                  {columnId === "active" && "Currently in development"}
                  {columnId === "completed" && "Finished and deployed"}
                </FrameDescription>
              </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>
  )
}
```
