Custom Shadcn Kanban for React and Tailwind CSS. A drag-and-drop kanban component designed for seamless item organization across customizable columns.
Browse 5 production-ready Shadcn Kanban components for dashboards, forms, and product UI. These examples use Base UI primitives from @base-ui/react and stay fully compatible with Shadcn Create so radius, color, and typography match your configured theme.
Browse all 5 Shadcn Kanban components for copy-ready layouts, dashboards, and forms built with Tailwind CSS in the ReUI library.
import {
Kanban,
KanbanBoard,
KanbanColumn,
KanbanColumnContent,
KanbanColumnHandle,
KanbanItem,
KanbanItemHandle,
KanbanOverlay,
} from "@/components/reui/kanban"<Kanban
value={columns}
onValueChange={setColumns}
getItemValue={(item) => item.id}
>
<KanbanBoard>
{Object.entries(columns).map(([id, items]) => (
<KanbanColumn key={id} value={id}>
<KanbanColumnHandle>
<h3>{id}</h3>
</KanbanColumnHandle>
<KanbanColumnContent value={id}>
{items.map((item) => (
<KanbanItem key={item.id} value={item.id}>
<KanbanItemHandle>{item.content}</KanbanItemHandle>
</KanbanItem>
))}
</KanbanColumnContent>
</KanbanColumn>
))}
</KanbanBoard>
<KanbanOverlay>
<div className="bg-muted size-full rounded-md" />
</KanbanOverlay>
</Kanban>The root component that provides the kanban context and manages the items state.
The horizontal container for kanban columns.
An individual column within the kanban board.
The drag handle for a column (if columns are sortable).
The scrollable area within a column that holds the items.
An individual draggable item within a column.
The drag handle for an individual item.
The ghost element displayed during a drag operation.
"use client"
import { ComponentProps, useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
Kanban,
KanbanBoard,
KanbanColumn,
KanbanColumnContent,
KanbanColumnHandle,
KanbanItem,
KanbanItemHandle,
KanbanOverlay,
} from "@/components/reui/kanban"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { GripVerticalIcon } from 'lucide-react'
interface Task {
id: string
title: string
priority: "low" | "medium" | "high"
description?: string
assignee?: string
assigneeAvatar?: string
dueDate?: string
}
const COLUMN_TITLES: Record<string, string> = {
backlog: "Backlog",
inProgress: "In Progress",
review: "Review",
done: "Done",
}
interface TaskCardProps extends Omit<
ComponentProps<typeof KanbanItem>,
"value" | "children"
> {
task: Task
asHandle?: boolean
isOverlay?: boolean
}
function TaskCard({ task, asHandle, isOverlay, ...props }: TaskCardProps) {
const cardContent = (
<Card>
<CardContent className="space-y-2.5">
<div className="flex items-center justify-between gap-2">
<span className="line-clamp-1 text-sm font-medium">{task.title}</span>
<Badge
variant={
task.priority === "high"
? "destructive-light"
: task.priority === "medium"
? "primary-light"
: "warning-light"
}
className="pointer-events-none h-5 shrink-0 rounded-sm px-1.5 text-xs capitalize"
>
{task.priority}
</Badge>
</div>
<div className="text-muted-foreground flex items-center justify-between text-xs">
{task.assignee && (
<div className="flex items-center gap-1">
<Avatar className="size-4">
<AvatarImage src={task.assigneeAvatar} />
<AvatarFallback>{task.assignee.charAt(0)}</AvatarFallback>
</Avatar>
<span className="line-clamp-1">{task.assignee}</span>
</div>
)}
{task.dueDate && (
<time className="text-[10px] whitespace-nowrap tabular-nums">
{task.dueDate}
</time>
)}
</div>
</CardContent>
</Card>
)
return (
<KanbanItem value={task.id} {...props}>
{asHandle && !isOverlay ? (
<KanbanItemHandle>{cardContent}</KanbanItemHandle>
) : (
cardContent
)}
</KanbanItem>
)
}
interface TaskColumnProps extends Omit<
ComponentProps<typeof KanbanColumn>,
"children"
> {
tasks: Task[]
isOverlay?: boolean
}
function TaskColumn({ value, tasks, isOverlay, ...props }: TaskColumnProps) {
return (
<KanbanColumn value={value} {...props}>
<Card className="mb-2.5">
<CardHeader className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<span className="text-sm font-semibold">
{COLUMN_TITLES[value]}
</span>
<Badge variant="outline">{tasks.length}</Badge>
</div>
<KanbanColumnHandle
render={(props) => (
<Button {...props} size="icon-xs" variant="ghost">
<GripVerticalIcon />
</Button>
)}
/>
</CardHeader>
<CardContent>
<KanbanColumnContent value={value} className="flex flex-col gap-2.5">
{tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
asHandle={!isOverlay}
isOverlay={isOverlay}
/>
))}
</KanbanColumnContent>
</CardContent>
</Card>
</KanbanColumn>
)
}
export function Pattern() {
const [columns, setColumns] = useState<Record<string, Task[]>>({
backlog: [
{
id: "1",
title: "Add authentication",
priority: "high",
assignee: "Alex Johnson",
assigneeAvatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
dueDate: "Jan 10, 2025",
},
{
id: "2",
title: "Create API endpoints",
priority: "medium",
assignee: "Sarah Chen",
assigneeAvatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
dueDate: "Jan 15, 2025",
},
{
id: "3",
title: "Write documentation",
priority: "low",
assignee: "Michael Rodriguez",
assigneeAvatar:
"https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
dueDate: "Jan 20, 2025",
},
],
inProgress: [
{
id: "4",
title: "Design system updates",
priority: "high",
assignee: "Emma Wilson",
assigneeAvatar:
"https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
dueDate: "Aug 25, 2025",
},
{
id: "5",
title: "Implement dark mode",
priority: "medium",
assignee: "David Kim",
assigneeAvatar:
"https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
dueDate: "Aug 25, 2025",
},
],
done: [
{
id: "7",
title: "Setup project",
priority: "high",
assignee: "Aron Thompson",
assigneeAvatar:
"https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
dueDate: "Sep 25, 2025",
},
{
id: "8",
title: "Initial commit",
priority: "low",
assignee: "James Brown",
assigneeAvatar:
"https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
dueDate: "Sep 20, 2025",
},
],
})
return (
<Kanban
value={columns}
onValueChange={setColumns}
getItemValue={(item) => item.id}
>
<KanbanBoard className="grid auto-rows-fr grid-cols-3">
{Object.entries(columns).map(([columnValue, tasks]) => (
<TaskColumn key={columnValue} value={columnValue} tasks={tasks} />
))}
</KanbanBoard>
<KanbanOverlay className="bg-muted/10 rounded-md border-2 border-dashed" />
</Kanban>
)
}
"use client"
import { ComponentProps, useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
Kanban,
KanbanBoard,
KanbanColumn,
KanbanColumnContent,
KanbanColumnHandle,
KanbanItem,
KanbanItemHandle,
KanbanOverlay,
} from "@/components/reui/kanban"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { GripVerticalIcon } from 'lucide-react'
interface Task {
id: string
title: string
priority: "low" | "medium" | "high"
description?: string
assignee?: string
assigneeAvatar?: string
dueDate?: string
}
const COLUMN_TITLES: Record<string, string> = {
backlog: "Backlog",
inProgress: "In Progress",
review: "Review",
done: "Done",
}
interface TaskCardProps extends Omit<
ComponentProps<typeof KanbanItem>,
"value" | "children"
> {
task: Task
asHandle?: boolean
isOverlay?: boolean
}
function TaskCard({ task, asHandle, isOverlay, ...props }: TaskCardProps) {
const cardContent = (
<Card>
<CardContent className="flex flex-col gap-2.5">
<div className="flex items-center justify-between gap-2">
<span className="line-clamp-1 text-sm font-medium">{task.title}</span>
<Badge
variant={
task.priority === "high"
? "destructive-outline"
: task.priority === "medium"
? "primary-outline"
: "warning-outline"
}
className="pointer-events-none h-5 shrink-0 rounded-sm px-1.5 text-[11px] capitalize"
>
{task.priority}
</Badge>
</div>
<div className="text-muted-foreground flex items-center justify-between text-xs">
{task.assignee && (
<div className="flex items-center gap-1">
<Avatar className="size-4">
<AvatarImage src={task.assigneeAvatar} />
<AvatarFallback>{task.assignee.charAt(0)}</AvatarFallback>
</Avatar>
<span className="line-clamp-1">{task.assignee}</span>
</div>
)}
{task.dueDate && (
<time className="text-[10px] whitespace-nowrap tabular-nums">
{task.dueDate}
</time>
)}
</div>
</CardContent>
</Card>
)
return (
<KanbanItem value={task.id} {...props}>
{asHandle && !isOverlay ? (
<KanbanItemHandle>{cardContent}</KanbanItemHandle>
) : (
cardContent
)}
</KanbanItem>
)
}
interface TaskColumnProps extends Omit<
ComponentProps<typeof KanbanColumn>,
"children"
> {
tasks: Task[]
isOverlay?: boolean
}
function TaskColumn({ value, tasks, isOverlay, ...props }: TaskColumnProps) {
return (
<KanbanColumn value={value} {...props}>
<Card className="mb-2.5">
<CardHeader className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<span className="text-sm font-semibold">
{COLUMN_TITLES[value]}
</span>
<Badge variant="outline">{tasks.length}</Badge>
</div>
<KanbanColumnHandle
render={(props) => (
<Button {...props} size="icon-xs" variant="ghost">
<GripVerticalIcon />
</Button>
)}
/>
</CardHeader>
<CardContent>
<KanbanColumnContent
value={value}
className="flex flex-col gap-2.5 p-0.5"
>
{tasks.map((task) => (
<TaskCard key={task.id} task={task} asHandle={!isOverlay} />
))}
</KanbanColumnContent>
</CardContent>
</Card>
</KanbanColumn>
)
}
export function Pattern() {
const [columns, setColumns] = useState<Record<string, Task[]>>({
backlog: [
{
id: "1",
title: "Add authentication",
priority: "high",
assignee: "Alex Johnson",
assigneeAvatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
dueDate: "Jan 10, 2025",
},
{
id: "2",
title: "Create API endpoints",
priority: "medium",
assignee: "Sarah Chen",
assigneeAvatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
dueDate: "Jan 15, 2025",
},
{
id: "3",
title: "Write documentation",
priority: "low",
assignee: "Michael Rodriguez",
assigneeAvatar:
"https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
dueDate: "Jan 20, 2025",
},
],
inProgress: [
{
id: "4",
title: "Design system updates",
priority: "high",
assignee: "Emma Wilson",
assigneeAvatar:
"https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
dueDate: "Aug 25, 2025",
},
{
id: "5",
title: "Implement dark mode",
priority: "medium",
assignee: "David Kim",
assigneeAvatar:
"https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
dueDate: "Aug 25, 2025",
},
],
done: [
{
id: "7",
title: "Setup project",
priority: "high",
assignee: "Aron Thompson",
assigneeAvatar:
"https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
dueDate: "Sep 25, 2025",
},
{
id: "8",
title: "Initial commit",
priority: "low",
assignee: "James Brown",
assigneeAvatar:
"https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
dueDate: "Sep 20, 2025",
},
],
})
return (
<Kanban
value={columns}
onValueChange={setColumns}
getItemValue={(item) => item.id}
>
<KanbanBoard className="grid auto-rows-fr grid-cols-3">
{Object.entries(columns).map(([columnValue, tasks]) => (
<TaskColumn key={columnValue} value={columnValue} tasks={tasks} />
))}
</KanbanBoard>
<KanbanOverlay>
{({ value, variant }) => {
if (variant === "column") {
const tasks = columns[value] ?? []
return <TaskColumn value={String(value)} tasks={tasks} isOverlay />
}
const task = Object.values(columns)
.flat()
.find((task) => task.id === value)
if (!task) return null
return <TaskCard task={task} isOverlay />
}}
</KanbanOverlay>
</Kanban>
)
}