Toots includes a visual Kanban board powered by dnd-kit that lets you organize tickets, track progress, and update status with drag-and-drop.
Board overview
The Kanban board displays three columns:
To do — Work that hasn’t started yet. New tickets appear here by default.
In progress — Active work currently being executed.
Done — Completed work. Tickets here are finished.
Each column shows ticket count and displays tickets sorted by sortOrder (the order AI generated them in, or your manual reordering).
Ticket cards
Each ticket appears as a card with key information:
// Visual representation
┌─────────────────────────────────────┐
│ ◯ Design user onboarding mockups │ ← Title with status icon
│ │
│ 🔵 Story 🔴 P0 M │ ← Type , Priority , Effort
│ │
│ Created Jan 15 │ ← Creation date
└─────────────────────────────────────┘
Status icons
Circle — To do (not started)
Circle with dot — In progress
Check circle (green) — Done
Priority badges
Priorities display with color-coded backgrounds:
P0 — Red background (critical, urgent)
P1 — Orange background (high priority)
P2 — Yellow background (medium priority)
P3 — Blue background (low priority)
Type indicators
Ticket types show with a colored dot:
Story — Blue dot
Task — Green dot
Epic — Purple dot
Milestone — Amber dot
Deliverable — Red dot
Drag and drop
Move tickets between columns by dragging:
Click and hold a ticket card
Drag to the target column (To do, In progress, or Done)
Release to drop — status updates automatically
The board uses dnd-kit’s pointer sensor with an 8-pixel activation threshold to prevent accidental drags when clicking to view ticket details.
Status changes persist immediately to PostgreSQL. If the update fails, the ticket reverts to its previous status.
Drag overlay
While dragging, a semi-transparent overlay follows your cursor, and the original card becomes faded. This provides clear visual feedback about what you’re moving.
Viewing ticket details
Click any ticket card to open a slide-out detail sheet with full information:
Complete description
All acceptance criteria
Dependencies (linked ticket IDs)
Labels for categorization
Created and updated timestamps
From the detail sheet, you can:
Edit any ticket field
Delete the ticket (with confirmation)
View dependencies as clickable links
Ticket deletions are permanent and cannot be undone. The detail sheet requires confirmation before deleting.
How drag-and-drop works
The Kanban board is built with @dnd-kit/core , a modern React drag-and-drop library:
DndContext setup
const sensors = useSensors (
useSensor ( PointerSensor , {
activationConstraint: { distance: 8 },
})
)
< DndContext
sensors = { sensors }
onDragStart = { handleDragStart }
onDragEnd = { handleDragEnd }
>
{ /* Kanban columns */ }
</ DndContext >
The pointer sensor requires 8 pixels of movement before activating — this prevents accidental drags when you’re trying to click to view details.
Handling drops
When you drop a ticket on a column:
const handleDragEnd = async ( event : DragEndEvent ) => {
const { active , over } = event
if ( ! over ) return
const ticketId = String ( active . id )
const newStatus = over . id as KanbanStatus // "todo", "in-progress", or "done"
// Optimistic update
setTickets (( prev ) =>
prev . map (( t ) => ( t . id === ticketId ? { ... t , status: newStatus } : t ))
)
try {
await rpc . tickets . updateStatus ({ id: ticketId , status: newStatus })
} catch {
setTickets ( previousTickets ) // Revert on error
toast . error ( "Failed to update ticket status" )
}
}
The board uses optimistic updates for instant visual feedback, then persists changes to the database. If the API call fails, the UI reverts.
Droppable columns
Each column is a droppable zone:
const { setNodeRef } = useDroppable ({
id: status , // "todo", "in-progress", or "done"
})
< div ref = { setNodeRef } className = "kanban-column" >
{ tickets . map (( ticket ) => (
< DraggableTicketCard key = { ticket . id } ticket = { ticket } />
)) }
</ div >
Draggable tickets
Each ticket card hooks into dnd-kit:
const { attributes , listeners , setNodeRef , transform , isDragging } = useDraggable ({
id: ticket . id ,
data: { ticket },
})
const style = transform
? { transform: CSS . Translate . toString ( transform ) }
: undefined
< TicketCard
ref = { setNodeRef }
style = { style }
className = { isDragging && "opacity-30" }
{ ... listeners }
{ ... attributes }
/>
The transform prop provides smooth animations during drag, and isDragging fades the original card while showing the overlay.
Ticket grouping and sorting
Tickets are grouped by status and sorted within each column:
function groupByStatus ( tickets : Ticket []) : Record < KanbanStatus , Ticket []> {
const groups = {
todo: [] as Ticket [],
"in-progress" : [] as Ticket [],
done: [] as Ticket []
}
for ( const t of tickets ) {
const status = ( t . status in groups ? t . status : "todo" ) as KanbanStatus
groups [ status ]. push ( t )
}
// Sort by sortOrder within each column
for ( const status of KANBAN_STATUSES ) {
groups [ status ]. sort (( a , b ) => a . sortOrder - b . sortOrder )
}
return groups
}
The sortOrder field preserves the order the AI generated tickets in, which typically follows logical dependency order (planning → execution → launch).
Real-time persistence
Every status change triggers an immediate database update:
// RPC call to update ticket status
await rpc . tickets . updateStatus ({
id: ticketId ,
status: newStatus
})
The backend updates the Prisma Ticket model:
model Ticket {
id String @id @default ( cuid ())
projectId String
status String @default ( "todo" ) // "todo", "in-progress", "done"
sortOrder Int @default ( 0 )
// ... other fields
}
Status is stored as a string enum in PostgreSQL, allowing future expansion to additional workflow stages.
Responsive design
The board adapts to different screen sizes:
Desktop — All three columns visible side-by-side
Tablet — Horizontal scroll with columns at fixed width
Mobile — Swipe to navigate between columns
The container uses overflow-x-auto with pb-4 for scrollbar clearance:
< div className = "flex gap-4 overflow-x-auto pb-4 h-full" >
{ KANBAN_STATUSES . map (( status ) => (
< KanbanColumn key = { status } status = { status } tickets = { groups [ status ] } />
)) }
</ div >
Keyboard accessibility
While dnd-kit primarily supports pointer-based dragging, you can navigate the board with keyboard:
Tab to focus a ticket card
Enter to open the detail sheet
Escape to close the detail sheet
Use the detail sheet’s status dropdown to change status without dragging
Full keyboard-based drag-and-drop is on the roadmap. For now, use the ticket detail sheet to update status via dropdown.
Next steps
AI ticket generation Learn how tickets are created
Project chat Refine your board with AI assistance