VibeWeek
Home/Grow/Drag-and-Drop & Kanban Boards

Drag-and-Drop & Kanban Boards

⬅️ Day 6: Grow Overview

If you're building any B2B SaaS that touches workflow, project management, sales pipeline, support tickets, content production, or task lists, you'll eventually need a kanban board with drag-and-drop. Linear, Trello, Asana, Notion, Pipedrive, Jira, GitHub Projects — all have one. The naive implementation: HTML5 native drag-and-drop. The structured implementation: dnd-kit (or Framer Motion / TanStack DnD) with optimistic updates, position-stable database persistence, accessibility, and touch support. The patterns below come from how Linear and Notion build their boards — both do roughly the same thing under different visual skins.

1. Decide: native HTML5 vs library

Decide between native HTML5 drag-and-drop and a library.

Native HTML5 (draggable + dragstart/dragover/drop):
- Pros: zero deps; works in browsers
- Cons: clunky API; broken touch support; broken accessibility; flicker; no animation
- Verdict: don't use for product UIs in 2026

dnd-kit (recommended for React in 2026):
- Pros: accessible (keyboard / screen reader); touch + mouse; performant; modular
- Cons: learning curve; setup boilerplate
- Verdict: default choice for React kanban / sortable lists

Framer Motion (animations + drag):
- Pros: gorgeous animations; spring physics
- Cons: not optimized for collision detection / sortable lists
- Verdict: use for delightful single-item drag (bottom sheets, image carousels), not multi-item kanban

TanStack DnD (formerly react-dnd):
- Pros: mature; type-safe; works with TanStack Table
- Cons: less popular than dnd-kit in 2026
- Verdict: legacy alternative

Vue / Svelte equivalents:
- Vue.draggable.next (built on Sortable.js)
- svelte-dnd-action

For React in 2026: default to dnd-kit. Output:
1. Install command
2. Core abstractions: DndContext, useSortable, DragOverlay, KeyboardSensor
3. Minimal kanban boilerplate (3 columns, draggable cards)
4. Accessibility primitives included by default

The reason library matters: native HTML5 drag-and-drop is broken on mobile, fails accessibility audits, and requires per-browser hacks. dnd-kit handles all three.

2. Data model — position-stable ordering

Store positions correctly or your kanban will reorder spontaneously. The classic mistake: integer positions (1, 2, 3) that need reshuffling on every move.

Design position storage for a kanban board.

Bad approach: integer ranks (1, 2, 3, 4)
- Move card from 4 to 1 → renumber 1→2, 2→3, 3→4 (3 writes)
- Race condition: two users move at once → corruption

Better approach: float ranks (1.0, 2.0, 3.0)
- Move card to between 1.0 and 2.0 → set rank = 1.5
- Reshuffle when too many decimals accumulate
- Float precision eventually exhausts (after ~50 reorderings)

Best approach: lexicographic / fractional indexing (LexoRank)
- Strings like "0|0000ai", "0|hzzzzr", "0|i00007"
- Insert between any two values without reshuffling
- LexoRank algorithm (used by Jira)
- Library: fractional-indexing on npm

Recommended: fractional-indexing for any production kanban.

Schema:
- card.column_id (which column)
- card.rank (string, lexicographic)
- Index on (column_id, rank) for O(log N) ordered query

Move operation:
- Frontend: compute new rank from neighbors (e.g., between cards X and Y → midpoint)
- Backend: validate column_id exists, write new rank
- Concurrent writes: each user writes their own rank; conflicts vanish

Output:
1. Schema (Postgres / your DB)
2. Migration to add rank column with backfill
3. Frontend rank-computation helper (insertBefore, insertAfter, insertBetween)
4. Backend update endpoint (POST /api/cards/:id/move with column_id + rank)

The fractional-indexing pattern is non-obvious. Jira invented it; LexoRank library packages the math. Expect 1-2 hours to wrap your head around it the first time; never think about it again after.

3. Optimistic UI — drag should feel instant

Implement optimistic kanban updates in React Query / SWR / TanStack Query.

Flow:
1. User drags card from column A to column B
2. Optimistically update local state (card moves in UI immediately)
3. Send PATCH /api/cards/:id { column_id: B, rank: ... } in background
4. On success: silent (UI already updated)
5. On failure: revert + toast error

Code structure:
- onDragEnd handler
- Compute new rank (between neighbors)
- queryClient.setQueryData(...) — optimistic update
- mutate(...) — send to server
- onError: rollback to previous data
- onSettled: invalidate query (re-fetch fresh data)

Edge cases:
- Card moved to same position → no-op
- Card moved while another user is moving same card → conflict (last-writer-wins or merge)
- Network failure → toast + revert + offer retry
- Slow network → spinner on the moving card after 500ms

Output:
1. onDragEnd full implementation
2. Conflict-handling strategy
3. Error-recovery UI (toast, retry, revert)
4. Performance: avoid re-rendering whole board on every dragOver

The performance trap: re-rendering the entire kanban during drag. Use React.memo on column components, useMemo on card lists, and rely on dnd-kit's drag-overlay to render the dragging card outside the column tree.

4. Server endpoint — atomic move with conflict detection

Build the move-card API endpoint.

Endpoint: PATCH /api/cards/:cardId/move
Body: { columnId: string, rank: string }

Validations:
- User has access to this board (RBAC)
- Card belongs to a board user can edit
- Target column exists on same board
- Rank is well-formed (lexicographic string)

Execution:
- Single SQL update, atomic:
  UPDATE cards SET column_id = $1, rank = $2, updated_at = NOW() WHERE id = $3 AND board_id = $4
- Return 200 with updated card
- 404 if card not found, 403 if no access

Conflict handling:
- If two users move the same card simultaneously: last writer wins
- If two users move different cards to same rank: fractional indexing avoids most conflicts
- Real-time sync: broadcast move via WebSocket / SSE so other users' boards update

Output:
1. Endpoint spec (request, response, errors)
2. SQL migration for cards table
3. Authorization middleware
4. WebSocket broadcast (optional, for multi-user real-time)
5. Audit log entry on move (for activity feed)

The audit-log entry: every kanban move is a noisy data point. Don't log "moved card 12 to rank 3.5" — log "moved 'Fix login bug' from To Do to In Progress." Human-readable for the activity feed.

5. Accessibility — keyboard + screen reader

The reason most teams ship inaccessible kanban: dnd-kit makes accessibility easy if you use the built-ins; impossible if you fight them.

Ensure kanban accessibility.

Required behaviors:
- Tab to focus a card
- Space / Enter to "pick up" (announces "picked up card X")
- Arrow keys to move (announces "moved to In Progress, position 2 of 5")
- Space / Enter to drop (announces "dropped card X in In Progress")
- Escape to cancel pickup

dnd-kit provides:
- KeyboardSensor (handles arrow keys)
- ScreenReaderInstructions (live region announcements)
- Default announcements (in English; localize for your locales)

Customizations needed:
- Override announcements for your domain ("moved 'Fix bug' from Backlog to In Progress")
- Visual focus rings on cards (not removed by Tailwind reset)
- Sufficient color contrast on focus + drag overlay
- Don't-rely-on-color-alone: use text labels for column status

Test:
- Tab through entire board
- Move 3 cards using only keyboard
- Test with VoiceOver (Mac) and NVDA (Windows)
- Run axe-core / Lighthouse accessibility audit
- Manual test with screen-reader off but keyboard-only

Output:
1. KeyboardSensor + screen-reader-instructions setup
2. Custom announcements for your domain
3. Focus-style CSS (Tailwind ring utilities)
4. Test plan with screen-reader steps
5. Mobile / touch parity (TouchSensor + sufficient drag-handle size)

Required, not optional: a kanban that fails keyboard navigation also fails Section 508, ADA, EAA (EU). Ship inaccessible at your legal risk.

6. Touch / mobile support

Make kanban work on mobile.

Touch challenges:
- Long-press to start drag (avoid scroll conflict)
- Visible drag handle (small target = miss-prone)
- Auto-scroll columns when dragging near edge
- Vibration / haptic feedback on pickup (mobile only)

dnd-kit:
- TouchSensor with activationConstraint (delay 250ms + tolerance 5px)
- AutoScrollPlugin for column edges
- DragOverlay for drag preview

iOS-specific:
- Disable text selection during drag (-webkit-user-select: none)
- Prevent overscroll bounce (touch-action: none on sortable)

Android-specific:
- Test with TalkBack (screen reader)
- Vibration via navigator.vibrate(50) on pickup

Common fail patterns:
- Drag conflicts with scroll → users can't scroll past kanban
- Drag handle too small → mis-touches
- No haptic feedback → drag doesn't feel "picked up"

Output:
1. TouchSensor + activationConstraint config
2. Drag-handle component (visible chevron / dots indicator)
3. Auto-scroll setup
4. Mobile-specific CSS (touch-action, user-select)
5. Test plan: iPhone Safari, Android Chrome, with VoiceOver / TalkBack

Touch is where most kanbans break. Ship desktop-first if you must, but plan touch testing.

7. Real-time collaboration — multi-user moves

If two users see the same board, they should see each other's moves live.

Add real-time sync to kanban.

Stack options:
- WebSocket / Socket.io (custom infra)
- Pusher / Ably / Convex (managed)
- Supabase Realtime (built-in to Postgres changes)
- Liveblocks (collaboration-focused)
- Yjs / Automerge (CRDT-based, fully offline-capable)

Pattern:
1. User A moves card → optimistic local update + PATCH to server
2. Server commits to DB → emits event to channel: kanban:board-id
3. User B's client subscribed to channel → receives event → updates local state
4. User B sees card move with smooth animation

Conflict scenarios:
- Both users move same card → last writer wins (client-side animation will visibly correct)
- User A drags while User B's update arrives → defer applying remote update until A's drag ends

Performance:
- Don't broadcast every dragOver event (huge load); only broadcast onDragEnd
- Debounce on rapid moves (e.g., reordering 10 cards in 5 seconds → batch)
- Skip own broadcast (each client filters out events from itself)

Output:
1. Real-time architecture choice + rationale
2. Channel naming convention (kanban:board-{id})
3. Event payload schema (move event with cardId, columnId, rank, userId)
4. Client subscription + reconciliation logic
5. Conflict / drag-deferred-update strategy
6. Performance limits + cleanup on unmount

The defer-during-drag rule: if user A is mid-drag and a remote update arrives for the same card, queue the remote update. Apply when A drops. Otherwise the card flickers to two positions.

8. Common views — kanban, list, calendar, gantt

Most products eventually want kanban + alternate views. Plan the data model so views are interchangeable.

Design data model for view-switchable workspace.

Card model:
- id, title, description, status, priority, due_date, assignee, ...
- column_id (kanban grouping)
- rank (kanban order)
- One card; many views

Views:
- Kanban (group by status / column_id)
- List (sort by created_at / due_date)
- Calendar (group by due_date)
- Gantt (start_date + duration)
- Timeline (sort by start_date)

UI:
- View switcher tab/button
- Each view has its own URL state (sort, filter, group_by)
- Filters apply across views
- Saved views = saved view-config

Tradeoffs:
- Don't model each view's data separately — ONE source of truth, many projections
- View switcher should be instant (no re-fetch if data already loaded)
- Calendar/Gantt require date fields populated (handle nulls gracefully)

Output:
1. Card schema with multi-view fields (status, due_date, start_date, priority)
2. View switcher component
3. Per-view UI components (Kanban / List / Calendar / Gantt)
4. URL state per view
5. Loading + empty states per view (cards without dates → "Add date" prompt in Calendar view)

The view-switcher pattern is what makes Linear and Notion feel powerful. The first kanban is just kanban; the next iteration adds list/calendar/timeline at minor incremental cost.

9. Card detail — opens without losing board context

Design card detail interaction.

Two patterns:
Pattern A: Side panel (Linear, GitHub Projects)
- Click card → side panel slides from right
- Board stays visible behind
- Edit card → real-time updates to board card
- URL includes card ID for deep-linking (?card=abc123)

Pattern B: Modal (Trello, Asana)
- Click card → modal overlays board
- Background dimmed
- Close → back to board

Decision criteria:
- Side panel for productivity tools (Linear, project management)
- Modal for casual / mobile-first tools (Trello)

Either pattern, requirements:
- Deep-linkable URL (?card=abc123)
- Browser back/forward works
- Escape to close
- Click outside to close (modal) or stay open (panel)
- Keyboard navigation between cards (J/K or arrows on board, then Enter to open)

Edit interactions:
- Inline edit for title (click to edit)
- Description: rich-text editor (see rich-text-editor-implementation-chat)
- Comments thread
- Activity log
- Attachments
- Labels / due date / assignee

Output:
1. Side-panel vs modal recommendation for your product
2. URL state for card-deep-link
3. Keyboard shortcuts (J/K next/prev card, X to delete)
4. Inline-edit pattern
5. Card-detail tabs (Comments / Activity / Files)

Linear's side-panel pattern is widely copied because it preserves context. You see the board behind the panel; you can compare cards by clicking different ones in sequence.

10. Performance — virtualize long columns, batch renders

When a column has 500+ cards, naive rendering tanks performance.

Optimize kanban performance for large boards.

Frontend:
- Virtualize long columns (TanStack Virtual)
- Memoize Card components (React.memo with shallow compare)
- Use DragOverlay (renders dragging card outside column tree)
- Don't re-render entire board on dragOver; only the columns involved
- useDeferredValue for filter input (don't filter on every keystroke)

Backend:
- Index (board_id, column_id, rank) for ordered query
- Pagination per column (load first 50 cards; load more on scroll)
- Real-time subscription per board (not per column)

Performance budgets:
- Initial board paint: <500ms after data loads
- Drag-and-drop interaction: 60 fps (no jank)
- Card move PATCH: <200ms p95
- Column scroll: 60 fps even at 500 cards

Monitoring:
- Web Vitals INP for drag interactions
- Server-side: PATCH /move latency
- Real-time event delivery latency

Output:
1. Virtualization setup for long columns
2. React.memo + useMemo strategy
3. Performance test (drag with 500 cards in column)
4. Monitoring / observability hooks

The fast-kanban formula: virtualize, memoize, paginate, defer-filter. Skip any one and the board feels sluggish.

What Done Looks Like

A v1 kanban board for B2B SaaS in 2026:

  • dnd-kit for drag-and-drop (React) or framework equivalent
  • Fractional / lexicographic ranks for position storage
  • Optimistic UI updates with rollback on error
  • Keyboard accessibility (Tab + Space + Arrow keys + Escape)
  • Touch + mobile support (long-press to drag)
  • Card detail panel/modal with deep-link URL
  • Real-time sync if multi-user (WebSocket / Pusher / Convex / Supabase Realtime)
  • Audit log entry on each move (human-readable)
  • Performance budgets met (60 fps drag, <200ms move PATCH)

Add later when product is mature:

  • Alternate views (List / Calendar / Gantt) sharing same data
  • Filters / saved views per board
  • Bulk drag (multi-select cards)
  • WIP limits per column
  • Swimlanes (group cards by assignee within column)
  • Card customization (custom fields, types)

The mistake to avoid: integer ranks. Switching to fractional indexing later requires a data migration; doing it on day one costs nothing.

The second mistake: rolling your own drag-and-drop with HTML5. Your kanban will be broken on mobile, inaccessible, and visibly slower than dnd-kit-based competitors.

The third mistake: no real-time sync in a multi-user product. Users assume "live" in 2026; refresh-to-see-other-people's-work feels broken.

See Also