Build a Trello-style task management app with V0 featuring kanban boards, drag-and-drop using dnd-kit, task assignment with avatars, due dates, priority badges, and real-time board updates. You'll create project workspaces with list and board views using Next.js and Supabase — all in about 1-2 hours.
What you're building
Task management is essential for any team — whether you are tracking product features, client deliverables, or personal goals. A kanban board gives visual clarity on what is in progress, what is blocked, and what is done.
V0 generates the kanban layout, task cards, and drag-and-drop logic from prompts. Ask V0 to add @dnd-kit/core and @dnd-kit/sortable for the drag-and-drop functionality. Supabase stores projects, columns, and tasks with position-based ordering for drag reordering.
The architecture uses Server Components for the project list, a client component for the interactive kanban board with dnd-kit, Server Actions for task mutations (create, move, update, delete), and Dialog components for the task detail editor.
Final result
A kanban task manager with drag-and-drop boards, task assignment, priority tracking, due dates, project workspaces, and real-time position persistence.
Tech stack
Prerequisites
- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Supabase Auth configured for user authentication
- Basic understanding of kanban boards (columns with cards you can drag between)
Build steps
Set up the project and task database schema
Open V0 and create a new project. Use the Connect panel to add Supabase. Create the projects, columns, tasks, and project_members tables with position columns for drag-and-drop ordering.
1// Paste this prompt into V0's AI chat:2// Create a Supabase schema for a task management app:3// 1. projects table: id (uuid PK), name (text), description (text), owner_id (uuid FK), created_at (timestamptz)4// 2. columns table: id (uuid PK), project_id (uuid FK), name (text — 'To Do', 'In Progress', 'Review', 'Done'), position (int for ordering)5// 3. tasks table: id (uuid PK), column_id (uuid FK), title (text NOT NULL), description (text), assignee_id (uuid FK nullable), priority (text DEFAULT 'medium'), due_date (date nullable), position (int for ordering within column), created_at (timestamptz), updated_at (timestamptz)6// 4. project_members table: project_id (uuid FK), user_id (uuid FK), role (text DEFAULT 'member'), PRIMARY KEY(project_id, user_id)7// RLS: project_members can access their project's columns and tasks.8// Seed a sample project with 4 columns and 8 tasks across columns.Pro tip: Use V0's prompt queuing — queue the schema prompt, then 'add @dnd-kit/core and @dnd-kit/sortable to the project', then the kanban board component prompt.
Expected result: Four tables created with RLS policies, a sample project seeded with 4 columns and 8 tasks distributed across them.
Build the kanban board with drag-and-drop
Create the kanban board as a client component using @dnd-kit for drag-and-drop between columns. Each column is a droppable area, each task card is draggable. Dropping a task updates its column_id and position.
1'use client'23import { useState } from 'react'4import { DndContext, closestCorners, DragEndEvent } from '@dnd-kit/core'5import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'6import { Card, CardContent } from '@/components/ui/card'7import { Badge } from '@/components/ui/badge'8import { Avatar, AvatarFallback } from '@/components/ui/avatar'9import { Button } from '@/components/ui/button'10import { ScrollArea } from '@/components/ui/scroll-area'11import { moveTask } from '@/app/actions/tasks'1213type Task = {14 id: string15 title: string16 priority: string17 assignee_id: string | null18 due_date: string | null19 position: number20 column_id: string21}2223type Column = {24 id: string25 name: string26 position: number27 tasks: Task[]28}2930const priorityColors: Record<string, string> = {31 high: 'bg-red-100 text-red-800',32 medium: 'bg-yellow-100 text-yellow-800',33 low: 'bg-green-100 text-green-800',34}3536export function KanbanBoard({ initialColumns }: { initialColumns: Column[] }) {37 const [columns, setColumns] = useState(initialColumns)3839 async function handleDragEnd(event: DragEndEvent) {40 const { active, over } = event41 if (!over) return4243 const taskId = active.id as string44 const targetColumnId = over.id as string4546 // Optimistic update47 setColumns((prev) => {48 const updated = prev.map((col) => ({49 ...col,50 tasks: col.tasks.filter((t) => t.id !== taskId),51 }))52 const targetCol = updated.find((c) => c.id === targetColumnId)53 const task = prev.flatMap((c) => c.tasks).find((t) => t.id === taskId)54 if (targetCol && task) {55 targetCol.tasks.push({ ...task, column_id: targetColumnId, position: targetCol.tasks.length })56 }57 return updated58 })5960 await moveTask(taskId, targetColumnId, 0)61 }6263 return (64 <DndContext collisionDetection={closestCorners} onDragEnd={handleDragEnd}>65 <div className="flex gap-4 overflow-x-auto p-4">66 {columns.map((column) => (67 <div key={column.id} className="w-72 flex-shrink-0">68 <h3 className="font-semibold mb-3 flex items-center gap-2">69 {column.name}70 <Badge variant="secondary">{column.tasks.length}</Badge>71 </h3>72 <SortableContext items={column.tasks.map((t) => t.id)} strategy={verticalListSortingStrategy}>73 <ScrollArea className="space-y-2">74 {column.tasks.map((task) => (75 <Card key={task.id} className="mb-2 cursor-grab">76 <CardContent className="p-3">77 <p className="font-medium text-sm">{task.title}</p>78 <div className="flex items-center justify-between mt-2">79 <Badge className={priorityColors[task.priority]} variant="secondary">80 {task.priority}81 </Badge>82 {task.assignee_id && (83 <Avatar className="w-6 h-6">84 <AvatarFallback className="text-xs">U</AvatarFallback>85 </Avatar>86 )}87 </div>88 </CardContent>89 </Card>90 ))}91 </ScrollArea>92 </SortableContext>93 <Button variant="ghost" size="sm" className="w-full mt-2">+ Add task</Button>94 </div>95 ))}96 </div>97 </DndContext>98 )99}Pro tip: Use Design Mode (Option+D) to visually adjust card sizing, column widths, and the color palette for priority Badges at zero credit cost.
Expected result: A kanban board with four columns. Task cards are draggable between columns. Dropping a card immediately updates the UI and persists the change via Server Action.
Create Server Actions for task operations
Build Server Actions for creating tasks, moving tasks between columns, updating task details, and deleting tasks. The moveTask action updates both column_id and position atomically.
1'use server'23import { createClient } from '@/lib/supabase/server'4import { revalidatePath } from 'next/cache'56export async function createTask(projectId: string, columnId: string, title: string) {7 const supabase = await createClient()8 const { data: maxPos } = await supabase9 .from('tasks')10 .select('position')11 .eq('column_id', columnId)12 .order('position', { ascending: false })13 .limit(1)14 .single()1516 await supabase.from('tasks').insert({17 column_id: columnId,18 title,19 position: (maxPos?.position ?? 0) + 1,20 })21 revalidatePath(`/projects/${projectId}`)22}2324export async function moveTask(taskId: string, newColumnId: string, newPosition: number) {25 const supabase = await createClient()26 await supabase.from('tasks').update({27 column_id: newColumnId,28 position: newPosition,29 updated_at: new Date().toISOString(),30 }).eq('id', taskId)31}3233export async function updateTask(taskId: string, data: {34 title?: string35 description?: string36 priority?: string37 assignee_id?: string | null38 due_date?: string | null39}) {40 const supabase = await createClient()41 await supabase.from('tasks').update({42 ...data,43 updated_at: new Date().toISOString(),44 }).eq('id', taskId)45}4647export async function deleteTask(taskId: string, projectId: string) {48 const supabase = await createClient()49 await supabase.from('tasks').delete().eq('id', taskId)50 revalidatePath(`/projects/${projectId}`)51}Expected result: Server Actions handle all task CRUD operations. moveTask updates column_id and position for drag-and-drop persistence.
Build the task detail editor Dialog
Create a Dialog component that opens when clicking a task card. It shows the full task details with editable fields for title, description, priority, assignee, and due date.
1// Paste this prompt into V0's AI chat:2// Build a task detail editor Dialog component.3// When a user clicks a task card on the kanban board, open a shadcn/ui Dialog with:4// - Input for task title (editable)5// - Textarea for description6// - Select for priority (low, medium, high)7// - Select for assignee (list of project members with Avatar)8// - Calendar date picker for due date9// - Badge showing current column/status10// - Button row: Save, Delete (with AlertDialog confirmation), Close11// - Activity feed section showing when the task was created, moved, and edited12// Use Server Action updateTask for saving and deleteTask for removing.13// Use Form component for proper form handling.Expected result: A Dialog editor with editable task fields, Calendar date picker, assignee Select with member Avatars, and Save/Delete buttons.
Add the project list and alternative list view
Create the project list page and add a Tabs toggle on the board page to switch between kanban board and list view. The list view shows tasks in a sortable Table.
1// Paste this prompt into V0's AI chat:2// Build two views for the task management app:3// 1. Project list page at app/projects/page.tsx:4// - Server Component fetching all projects for the current user via project_members5// - Card for each project showing name, description, task count, member Avatars6// - "New Project" Button that opens a Dialog with Form for name and description7// 2. Add Tabs to app/projects/[id]/page.tsx to toggle between Board and List views:8// - Board tab: the KanbanBoard component9// - List tab: Table with columns for Title, Status (column name), Priority Badge, Assignee Avatar, Due Date, Actions DropdownMenu10// - Table rows should be sortable by clicking column headers11// Use Server Components for data fetching, client components only for interactive elements.Expected result: A project list page with Cards and a new project Dialog. The board page has Tabs switching between kanban board and sortable Table list view.
Complete code
1'use server'23import { createClient } from '@/lib/supabase/server'4import { revalidatePath } from 'next/cache'56export async function createTask(7 projectId: string,8 columnId: string,9 title: string10) {11 const supabase = await createClient()1213 const { data: maxPos } = await supabase14 .from('tasks')15 .select('position')16 .eq('column_id', columnId)17 .order('position', { ascending: false })18 .limit(1)19 .single()2021 await supabase.from('tasks').insert({22 column_id: columnId,23 title,24 position: (maxPos?.position ?? 0) + 1,25 })2627 revalidatePath(`/projects/${projectId}`)28}2930export async function moveTask(31 taskId: string,32 newColumnId: string,33 newPosition: number34) {35 const supabase = await createClient()36 await supabase37 .from('tasks')38 .update({39 column_id: newColumnId,40 position: newPosition,41 updated_at: new Date().toISOString(),42 })43 .eq('id', taskId)44}4546export async function updateTask(47 taskId: string,48 projectId: string,49 data: Record<string, unknown>50) {51 const supabase = await createClient()52 await supabase53 .from('tasks')54 .update({ ...data, updated_at: new Date().toISOString() })55 .eq('id', taskId)56 revalidatePath(`/projects/${projectId}`)57}5859export async function deleteTask(taskId: string, projectId: string) {60 const supabase = await createClient()61 await supabase.from('tasks').delete().eq('id', taskId)62 revalidatePath(`/projects/${projectId}`)63}Customization ideas
Add task labels and tags
Create a labels table with name and color. Let users assign multiple labels to tasks and filter the board by label.
Add time tracking
Add a timer component to task cards that tracks time spent. Store time entries in a separate table for reporting and invoicing.
Add subtasks and checklists
Add a subtasks table linked to parent tasks. Display completion percentage on the task card with a progress bar.
Add board templates
Let users create projects from templates with predefined columns and sample tasks for common workflows (Agile, Kanban, Bug Tracking).
Common pitfalls
Pitfall: Not using optimistic updates for drag-and-drop
How to avoid: Immediately update local state on drop, then fire the Server Action in the background. If the action fails, rollback to the snapshot state.
Pitfall: Using floating-point numbers for position ordering
How to avoid: Use integer positions with gaps (10, 20, 30). When inserting between 10 and 20, use 15. Periodically renormalize positions with a batch update.
Pitfall: Not scoping RLS to project membership
How to avoid: Create RLS policies that check project_members: SELECT on tasks requires the user to be a member of the task's column's project.
Best practices
- Use @dnd-kit for drag-and-drop — it is the most flexible React DnD library and works well with shadcn/ui Card components
- Implement optimistic drag-and-drop by updating local state immediately and persisting via Server Action in the background
- Use RLS policies joined through project_members to ensure users only access their own projects' tasks
- Use Design Mode (Option+D) to visually adjust Card sizing, column widths, and priority Badge colors at zero credit cost
- Use integer positions with gaps (10, 20, 30) for ordering to make reordering efficient without recalculating all positions
- Add Tabs for board vs list view to accommodate different user preferences — some people prefer tables over kanban
- Use connection pooling via Supavisor for concurrent board updates when multiple team members drag tasks simultaneously
AI prompts to try
Copy these prompts to build this project faster.
I'm building a kanban task management app with Next.js App Router, Supabase, and @dnd-kit. I need: 1) Draggable task cards between columns with optimistic updates, 2) Position-based ordering for cards within columns, 3) Task detail editor with assignee, priority, and due date, 4) RLS policies scoped to project membership. Help me design the schema and drag-and-drop state management.
Implement optimistic drag-and-drop for a kanban board using @dnd-kit and React state. On dragEnd: 1) Save a snapshot of the current columns state, 2) Immediately move the task card to the target column in local state, 3) Call a Server Action to persist the column_id and position change, 4) If the Server Action throws an error, restore the snapshot state. Handle edge cases: dropping on the same column (reorder only), dropping on an empty column, and cancelling the drag.
Frequently asked questions
What drag-and-drop library does this use?
@dnd-kit is a lightweight, flexible React drag-and-drop library. Install it by prompting V0: 'add @dnd-kit/core and @dnd-kit/sortable'. It handles both cross-column dragging and within-column reordering.
How does the position ordering work?
Each task has an integer position column. Tasks within a column are ordered by position ascending. When a task is dropped, its position is updated to reflect the new order. Using gaps (10, 20, 30) allows inserting between positions without recalculating all positions.
What V0 plan do I need?
V0 Free tier works. The kanban board uses standard React components with @dnd-kit, shadcn/ui Cards, and Supabase. Design Mode for visual polish is also free.
Can multiple team members use the board simultaneously?
Yes. Each user's drag-and-drop updates persist independently via Server Actions. For real-time sync, add Supabase Realtime subscriptions on the tasks table so other users see board changes instantly.
How do I add more columns to the board?
Insert a new row into the columns table with the project_id and a position value. The board component queries columns ordered by position, so new columns appear automatically.
Can RapidDev help build a custom project management tool?
Yes. RapidDev has built 600+ apps including project management platforms with Gantt charts, resource allocation, and sprint planning. Book a free consultation to discuss your workflow requirements.
How do I deploy this to production?
Click Share > Publish in V0. The Supabase connection is auto-configured from the Connect panel. The kanban board works identically in production — drag-and-drop, real-time updates, and all.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation