Data (4)
Shadcn.io is not affiliated with official shadcn/ui
Kanban
Agile workflow visualization that actually works. Move tasks between columns with smooth animations, built for React teams using Next.js who value clean code and great user experiences.
Powered by
Loading component...
'use client';import { faker } from '@faker-js/faker';import { KanbanBoard, KanbanCard, KanbanCards, KanbanHeader, KanbanProvider,} from '@/components/ui/shadcn-io/kanban';import { useState } from 'react';import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);const columns = [ { id: faker.string.uuid(), name: 'Planned', color: '#6B7280' }, { id: faker.string.uuid(), name: 'In Progress', color: '#F59E0B' }, { id: faker.string.uuid(), name: 'Done', color: '#10B981' },];const users = Array.from({ length: 4 }) .fill(null) .map(() => ({ id: faker.string.uuid(), name: faker.person.fullName(), image: faker.image.avatar(), }));const exampleFeatures = Array.from({ length: 20 }) .fill(null) .map(() => ({ id: faker.string.uuid(), name: capitalize(faker.company.buzzPhrase()), startAt: faker.date.past({ years: 0.5, refDate: new Date() }), endAt: faker.date.future({ years: 0.5, refDate: new Date() }), column: faker.helpers.arrayElement(columns).id, owner: faker.helpers.arrayElement(users), }));const dateFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric',});const shortDateFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric',});const Example = () => { const [features, setFeatures] = useState(exampleFeatures); return ( <KanbanProvider columns={columns} data={features} onDataChange={setFeatures} > {(column) => ( <KanbanBoard id={column.id} key={column.id}> <KanbanHeader> <div className="flex items-center gap-2"> <div className="h-2 w-2 rounded-full" style={{ backgroundColor: column.color }} /> <span>{column.name}</span> </div> </KanbanHeader> <KanbanCards id={column.id}> {(feature: (typeof features)[number]) => ( <KanbanCard column={column.id} id={feature.id} key={feature.id} name={feature.name} > <div className="flex items-start justify-between gap-2"> <div className="flex flex-col gap-1"> <p className="m-0 flex-1 font-medium text-sm"> {feature.name} </p> </div> {feature.owner && ( <Avatar className="h-4 w-4 shrink-0"> <AvatarImage src={feature.owner.image} /> <AvatarFallback> {feature.owner.name?.slice(0, 2)} </AvatarFallback> </Avatar> )} </div> <p className="m-0 text-muted-foreground text-xs"> {shortDateFormatter.format(feature.startAt)} -{' '} {dateFormatter.format(feature.endAt)} </p> </KanbanCard> )} </KanbanCards> </KanbanBoard> )} </KanbanProvider> );};export default Example;Sign in to view the code
'use client';import type { Announcements, DndContextProps, DragEndEvent, DragOverEvent, DragStartEvent,} from '@dnd-kit/core';import { closestCenter, DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useDroppable, useSensor, useSensors,} from '@dnd-kit/core';import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';import { CSS } from '@dnd-kit/utilities';import { createContext, type HTMLAttributes, type ReactNode, useContext, useState,} from 'react';import { createPortal } from 'react-dom';import tunnel from 'tunnel-rat';import { Card } from '@/components/ui/card';import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';import { cn } from '@/lib/utils';const t = tunnel();export type { DragEndEvent } from '@dnd-kit/core';type KanbanItemProps = { id: string; name: string; column: string;} & Record<string, unknown>;type KanbanColumnProps = { id: string; name: string;} & Record<string, unknown>;type KanbanContextProps< T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps,> = { columns: C[]; data: T[]; activeCardId: string | null;};const KanbanContext = createContext<KanbanContextProps>({ columns: [], data: [], activeCardId: null,});export type KanbanBoardProps = { id: string; children: ReactNode; className?: string;};export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => { const { isOver, setNodeRef } = useDroppable({ id, }); return ( <div className={cn( 'flex size-full min-h-40 flex-col divide-y overflow-hidden rounded-md border bg-secondary text-xs shadow-sm ring-2 transition-all', isOver ? 'ring-primary' : 'ring-transparent', className )} ref={setNodeRef} > {children} </div> );};export type KanbanCardProps<T extends KanbanItemProps = KanbanItemProps> = T & { children?: ReactNode; className?: string;};export const KanbanCard = <T extends KanbanItemProps = KanbanItemProps>({ id, name, children, className,}: KanbanCardProps<T>) => { const { attributes, listeners, setNodeRef, transition, transform, isDragging, } = useSortable({ id, }); const { activeCardId } = useContext(KanbanContext) as KanbanContextProps; const style = { transition, transform: CSS.Transform.toString(transform), }; return ( <> <div style={style} {...listeners} {...attributes} ref={setNodeRef}> <Card className={cn( 'cursor-grab gap-4 rounded-md p-3 shadow-sm', isDragging && 'pointer-events-none cursor-grabbing opacity-30', className )} > {children ?? <p className="m-0 font-medium text-sm">{name}</p>} </Card> </div> {activeCardId === id && ( <t.In> <Card className={cn( 'cursor-grab gap-4 rounded-md p-3 shadow-sm ring-2 ring-primary', isDragging && 'cursor-grabbing', className )} > {children ?? <p className="m-0 font-medium text-sm">{name}</p>} </Card> </t.In> )} </> );};export type KanbanCardsProps<T extends KanbanItemProps = KanbanItemProps> = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'id'> & { children: (item: T) => ReactNode; id: string; };export const KanbanCards = <T extends KanbanItemProps = KanbanItemProps>({ children, className, ...props}: KanbanCardsProps<T>) => { const { data } = useContext(KanbanContext) as KanbanContextProps<T>; const filteredData = data.filter((item) => item.column === props.id); const items = filteredData.map((item) => item.id); return ( <ScrollArea className="overflow-hidden"> <SortableContext items={items}> <div className={cn('flex flex-grow flex-col gap-2 p-2', className)} {...(props as any)} > {filteredData.map(children)} </div> </SortableContext> <ScrollBar orientation="vertical" /> </ScrollArea> );};export type KanbanHeaderProps = HTMLAttributes<HTMLDivElement>;export const KanbanHeader = ({ className, ...props }: KanbanHeaderProps) => ( <div className={cn('m-0 p-2 font-semibold text-sm', className)} {...(props as any)} />);export type KanbanProviderProps< T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps,> = Omit<DndContextProps, 'children'> & { children: (column: C) => ReactNode; className?: string; columns: C[]; data: T[]; onDataChange?: (data: T[]) => void; onDragStart?: (event: DragStartEvent) => void; onDragEnd?: (event: DragEndEvent) => void; onDragOver?: (event: DragOverEvent) => void;};export const KanbanProvider = < T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps,>({ children, onDragStart, onDragEnd, onDragOver, className, columns, data, onDataChange, ...props}: KanbanProviderProps<T, C>) => { const [activeCardId, setActiveCardId] = useState<string | null>(null); const sensors = useSensors( useSensor(MouseSensor), useSensor(TouchSensor), useSensor(KeyboardSensor) ); const handleDragStart = (event: DragStartEvent) => { const card = data.find((item) => item.id === event.active.id); if (card) { setActiveCardId(event.active.id as string); } onDragStart?.(event); }; const handleDragOver = (event: DragOverEvent) => { const { active, over } = event; if (!over) { return; } const activeItem = data.find((item) => item.id === active.id); const overItem = data.find((item) => item.id === over.id); if (!(activeItem)) { return; } const activeColumn = activeItem.column; const overColumn = overItem?.column || columns.find(col => col.id === over.id)?.id || columns[0]?.id; if (activeColumn !== overColumn) { let newData = [...data]; const activeIndex = newData.findIndex((item) => item.id === active.id); const overIndex = newData.findIndex((item) => item.id === over.id); newData[activeIndex].column = overColumn; newData = arrayMove(newData, activeIndex, overIndex); onDataChange?.(newData); } onDragOver?.(event); }; const handleDragEnd = (event: DragEndEvent) => { setActiveCardId(null); onDragEnd?.(event); const { active, over } = event; if (!over || active.id === over.id) { return; } let newData = [...data]; const oldIndex = newData.findIndex((item) => item.id === active.id); const newIndex = newData.findIndex((item) => item.id === over.id); newData = arrayMove(newData, oldIndex, newIndex); onDataChange?.(newData); }; const announcements: Announcements = { onDragStart({ active }) { const { name, column } = data.find((item) => item.id === active.id) ?? {}; return `Picked up the card "${name}" from the "${column}" column`; }, onDragOver({ active, over }) { const { name } = data.find((item) => item.id === active.id) ?? {}; const newColumn = columns.find((column) => column.id === over?.id)?.name; return `Dragged the card "${name}" over the "${newColumn}" column`; }, onDragEnd({ active, over }) { const { name } = data.find((item) => item.id === active.id) ?? {}; const newColumn = columns.find((column) => column.id === over?.id)?.name; return `Dropped the card "${name}" into the "${newColumn}" column`; }, onDragCancel({ active }) { const { name } = data.find((item) => item.id === active.id) ?? {}; return `Cancelled dragging the card "${name}"`; }, }; return ( <KanbanContext.Provider value={{ columns, data, activeCardId }}> <DndContext accessibility={{ announcements }} collisionDetection={closestCenter} onDragEnd={handleDragEnd} onDragOver={handleDragOver} onDragStart={handleDragStart} sensors={sensors} {...(props as any)} > <div className={cn( 'grid size-full auto-cols-fr grid-flow-col gap-4', className )} > {columns.map((column) => children(column))} </div> {typeof window !== 'undefined' && createPortal( <DragOverlay> <t.Out /> </DragOverlay>, document.body )} </DndContext> </KanbanContext.Provider> );};Sign in to view the source
Installation
npx shadcn@latest add https://www.shadcn.io/registry/kanban.jsonnpx shadcn@latest add https://www.shadcn.io/registry/kanban.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/kanban.jsonbunx shadcn@latest add https://www.shadcn.io/registry/kanban.jsonSign in to access installation commands
Sign inFeatures
- Smooth drag-and-drop built on DND Kit for seamless task movement across React Kanban columns
- Zero configuration setup ready to integrate into Next.js applications without complex configuration
- Fully customizable cards supporting descriptions, tags, avatars, and due dates with TypeScript safety
- Mobile-responsive design delivering consistent experience across phones, tablets, and desktop screens
- shadcn/ui integration ensuring seamless design system compatibility with existing React interfaces
- Complete accessibility featuring keyboard navigation, screen readers, and ARIA labels for inclusive use
- Performance optimized with hardware-accelerated transforms and efficient state management for JavaScript applications
- Open source freedom providing free, unlimited commercial use without vendor lock-in or monthly fees
Examples
Simple version
Loading component...
'use client';import { faker } from '@faker-js/faker';import type { DragEndEvent } from '@/components/ui/shadcn-io/kanban';import { KanbanBoard, KanbanCard, KanbanCards, KanbanHeader, KanbanProvider,} from '@/components/ui/shadcn-io/kanban';import { useState } from 'react';const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);const columns = [ { id: faker.string.uuid(), name: 'Planned', color: '#6B7280' }, { id: faker.string.uuid(), name: 'In Progress', color: '#F59E0B' }, { id: faker.string.uuid(), name: 'Done', color: '#10B981' },];const users = Array.from({ length: 4 }) .fill(null) .map(() => ({ id: faker.string.uuid(), name: faker.person.fullName(), image: faker.image.avatar(), }));const exampleFeatures = Array.from({ length: 20 }) .fill(null) .map(() => ({ id: faker.string.uuid(), name: capitalize(faker.company.buzzPhrase()), startAt: faker.date.past({ years: 0.5, refDate: new Date() }), endAt: faker.date.future({ years: 0.5, refDate: new Date() }), column: faker.helpers.arrayElement(columns).id, owner: faker.helpers.arrayElement(users), }));const Example = () => { const [features, setFeatures] = useState(exampleFeatures); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over) { return; } const status = columns.find(({ id }) => id === over.id); if (!status) { return; } setFeatures( features.map((feature) => { if (feature.id === active.id) { return { ...feature, column: status.id }; } return feature; }) ); }; return ( <KanbanProvider columns={columns} data={features} onDragEnd={handleDragEnd}> {(column) => ( <KanbanBoard id={column.id} key={column.id}> <KanbanHeader>{column.name}</KanbanHeader> <KanbanCards id={column.id}> {(feature) => ( <KanbanCard column={column.name} id={feature.id} key={feature.id} name={feature.name} /> )} </KanbanCards> </KanbanBoard> )} </KanbanProvider> );};export default Example;Sign in to view the code
'use client';import type { Announcements, DndContextProps, DragEndEvent, DragOverEvent, DragStartEvent,} from '@dnd-kit/core';import { closestCenter, DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useDroppable, useSensor, useSensors,} from '@dnd-kit/core';import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';import { CSS } from '@dnd-kit/utilities';import { createContext, type HTMLAttributes, type ReactNode, useContext, useState,} from 'react';import { createPortal } from 'react-dom';import tunnel from 'tunnel-rat';import { Card } from '@/components/ui/card';import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';import { cn } from '@/lib/utils';const t = tunnel();export type { DragEndEvent } from '@dnd-kit/core';type KanbanItemProps = { id: string; name: string; column: string;} & Record<string, unknown>;type KanbanColumnProps = { id: string; name: string;} & Record<string, unknown>;type KanbanContextProps< T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps,> = { columns: C[]; data: T[]; activeCardId: string | null;};const KanbanContext = createContext<KanbanContextProps>({ columns: [], data: [], activeCardId: null,});export type KanbanBoardProps = { id: string; children: ReactNode; className?: string;};export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => { const { isOver, setNodeRef } = useDroppable({ id, }); return ( <div className={cn( 'flex size-full min-h-40 flex-col divide-y overflow-hidden rounded-md border bg-secondary text-xs shadow-sm ring-2 transition-all', isOver ? 'ring-primary' : 'ring-transparent', className )} ref={setNodeRef} > {children} </div> );};export type KanbanCardProps<T extends KanbanItemProps = KanbanItemProps> = T & { children?: ReactNode; className?: string;};export const KanbanCard = <T extends KanbanItemProps = KanbanItemProps>({ id, name, children, className,}: KanbanCardProps<T>) => { const { attributes, listeners, setNodeRef, transition, transform, isDragging, } = useSortable({ id, }); const { activeCardId } = useContext(KanbanContext) as KanbanContextProps; const style = { transition, transform: CSS.Transform.toString(transform), }; return ( <> <div style={style} {...listeners} {...attributes} ref={setNodeRef}> <Card className={cn( 'cursor-grab gap-4 rounded-md p-3 shadow-sm', isDragging && 'pointer-events-none cursor-grabbing opacity-30', className )} > {children ?? <p className="m-0 font-medium text-sm">{name}</p>} </Card> </div> {activeCardId === id && ( <t.In> <Card className={cn( 'cursor-grab gap-4 rounded-md p-3 shadow-sm ring-2 ring-primary', isDragging && 'cursor-grabbing', className )} > {children ?? <p className="m-0 font-medium text-sm">{name}</p>} </Card> </t.In> )} </> );};export type KanbanCardsProps<T extends KanbanItemProps = KanbanItemProps> = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'id'> & { children: (item: T) => ReactNode; id: string; };export const KanbanCards = <T extends KanbanItemProps = KanbanItemProps>({ children, className, ...props}: KanbanCardsProps<T>) => { const { data } = useContext(KanbanContext) as KanbanContextProps<T>; const filteredData = data.filter((item) => item.column === props.id); const items = filteredData.map((item) => item.id); return ( <ScrollArea className="overflow-hidden"> <SortableContext items={items}> <div className={cn('flex flex-grow flex-col gap-2 p-2', className)} {...(props as any)} > {filteredData.map(children)} </div> </SortableContext> <ScrollBar orientation="vertical" /> </ScrollArea> );};export type KanbanHeaderProps = HTMLAttributes<HTMLDivElement>;export const KanbanHeader = ({ className, ...props }: KanbanHeaderProps) => ( <div className={cn('m-0 p-2 font-semibold text-sm', className)} {...(props as any)} />);export type KanbanProviderProps< T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps,> = Omit<DndContextProps, 'children'> & { children: (column: C) => ReactNode; className?: string; columns: C[]; data: T[]; onDataChange?: (data: T[]) => void; onDragStart?: (event: DragStartEvent) => void; onDragEnd?: (event: DragEndEvent) => void; onDragOver?: (event: DragOverEvent) => void;};export const KanbanProvider = < T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps,>({ children, onDragStart, onDragEnd, onDragOver, className, columns, data, onDataChange, ...props}: KanbanProviderProps<T, C>) => { const [activeCardId, setActiveCardId] = useState<string | null>(null); const sensors = useSensors( useSensor(MouseSensor), useSensor(TouchSensor), useSensor(KeyboardSensor) ); const handleDragStart = (event: DragStartEvent) => { const card = data.find((item) => item.id === event.active.id); if (card) { setActiveCardId(event.active.id as string); } onDragStart?.(event); }; const handleDragOver = (event: DragOverEvent) => { const { active, over } = event; if (!over) { return; } const activeItem = data.find((item) => item.id === active.id); const overItem = data.find((item) => item.id === over.id); if (!(activeItem)) { return; } const activeColumn = activeItem.column; const overColumn = overItem?.column || columns.find(col => col.id === over.id)?.id || columns[0]?.id; if (activeColumn !== overColumn) { let newData = [...data]; const activeIndex = newData.findIndex((item) => item.id === active.id); const overIndex = newData.findIndex((item) => item.id === over.id); newData[activeIndex].column = overColumn; newData = arrayMove(newData, activeIndex, overIndex); onDataChange?.(newData); } onDragOver?.(event); }; const handleDragEnd = (event: DragEndEvent) => { setActiveCardId(null); onDragEnd?.(event); const { active, over } = event; if (!over || active.id === over.id) { return; } let newData = [...data]; const oldIndex = newData.findIndex((item) => item.id === active.id); const newIndex = newData.findIndex((item) => item.id === over.id); newData = arrayMove(newData, oldIndex, newIndex); onDataChange?.(newData); }; const announcements: Announcements = { onDragStart({ active }) { const { name, column } = data.find((item) => item.id === active.id) ?? {}; return `Picked up the card "${name}" from the "${column}" column`; }, onDragOver({ active, over }) { const { name } = data.find((item) => item.id === active.id) ?? {}; const newColumn = columns.find((column) => column.id === over?.id)?.name; return `Dragged the card "${name}" over the "${newColumn}" column`; }, onDragEnd({ active, over }) { const { name } = data.find((item) => item.id === active.id) ?? {}; const newColumn = columns.find((column) => column.id === over?.id)?.name; return `Dropped the card "${name}" into the "${newColumn}" column`; }, onDragCancel({ active }) { const { name } = data.find((item) => item.id === active.id) ?? {}; return `Cancelled dragging the card "${name}"`; }, }; return ( <KanbanContext.Provider value={{ columns, data, activeCardId }}> <DndContext accessibility={{ announcements }} collisionDetection={closestCenter} onDragEnd={handleDragEnd} onDragOver={handleDragOver} onDragStart={handleDragStart} sensors={sensors} {...(props as any)} > <div className={cn( 'grid size-full auto-cols-fr grid-flow-col gap-4', className )} > {columns.map((column) => children(column))} </div> {typeof window !== 'undefined' && createPortal( <DragOverlay> <t.Out /> </DragOverlay>, document.body )} </DndContext> </KanbanContext.Provider> );};Sign in to view the source
Use Cases
This free open source React component works well for:
- Sprint planning - Moving user stories from backlog to in-progress to done with TypeScript safety
- Bug triage - Organizing issues by severity and tracking resolution progress in Next.js applications
- Content pipelines - Managing draft, review, approved, and published workflows using shadcn/ui design
- Sales CRM - Tracking leads through qualified, proposal, and closed stages with drag-and-drop functionality
- Support tickets - Processing new, assigned, resolved, and closed tickets with JavaScript interactivity
- Feature requests - Managing submitted, planned, development, and released features for React applications
- Hiring processes - Tracking candidates through applied, screening, interview, and decision stages
Implementation Notes
- DND Kit foundation handling collision detection, auto-scrolling, and smooth animations for React Kanban boards
- React Context state sharing board data without prop drilling using efficient JavaScript patterns
- Hardware acceleration with CSS transforms delivering 60fps dragging performance in Next.js applications
- Strategic memoization preventing unnecessary re-renders and scaling to hundreds of cards efficiently
- TypeScript integration providing fully typed interfaces for cards, columns, and drag events with shadcn/ui compatibility
- Portal management using efficient overlay systems that maintain layout integrity during drag operations
Accessibility
- Complete keyboard navigation supporting arrow keys, space, and enter for all Kanban interactions
- Screen reader announcements providing meaningful feedback for drag operations and state changes
- Focus management with proper indicators and logical tab order for React component accessibility
- ARIA compliance including live regions and high contrast theme support for inclusive design
- Universal compatibility ensuring JavaScript applications work for all users regardless of abilities
Gantt
Professional project timeline visualization with interactive scheduling capabilities. Built for React and Next.js teams who need powerful Gantt charts with TypeScript safety and modern shadcn/ui design.
List
Clean task lists with drag-and-drop organization for React and Next.js applications. Built with TypeScript support, shadcn/ui design, and smart grouping for simple yet powerful task management without complexity.