Drag-and-Drop & Kanban Boards
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
- Data Tables: Sort, Filter, Pagination, Bulk Actions — sister table-view pattern
- Real-Time Collaboration — collaboration plumbing
- Rich-Text Editor Implementation — used in card detail
- Keyboard Shortcuts & Command Palette — board-level shortcuts
- Toast Notifications UI — error feedback on drag failures
- Audit Logs — log card moves
- WebSocket / SSE Implementation — real-time transport
- Soft Delete vs Hard Delete — handling deleted cards
- VibeReference: Internal Tool Builders — Retool/Tooljet kanban widgets
- VibeReference: Customer Support Tools — kanban-like support queues
- LaunchWeek: Internal Tools Strategy — when to build vs buy