Skip to main content
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:
  1. Click and hold a ticket card
  2. Drag to the target column (To do, In progress, or Done)
  3. 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

Build docs developers (and LLMs) love