TrueSpec

Drag & Drop Kanban Board

dnd-kit drag and drop, column reorder, and state persistence

What You’ll Build

After following this guide, you will have a working implementation of drag & drop kanban board in your project. Build a Trello-style kanban board with drag and drop using @dnd-kit (the modern React DnD library). Cards can be dragged between columns, columns can be reordered, and the layout persists. Fully accessible with keyboard support.

Use Cases & Problems Solved

  • Implement interactive UI features that users expect from modern apps
  • Follow established patterns that scale and remain maintainable
  • Reduce boilerplate and avoid common frontend pitfalls

Prerequisites

  • React 18+
  • Basic React state management

Step-by-Step Implementation

Install dnd-kit

The following snippet shows how to install dnd-kit. Copy this into your project and adjust the values for your environment.

npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Sortable kanban column

The following snippet shows how to sortable kanban column. Copy this into your project and adjust the values for your environment.

import { DndContext, closestCorners, DragOverlay } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

function SortableCard({ id, title }) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
  const style = { transform: CSS.Transform.toString(transform), transition };

  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}
      className="p-4 bg-white rounded shadow cursor-grab active:cursor-grabbing">
      {title}
    </div>
  );
}

function Column({ id, title, cards }) {
  return (
    <div className="bg-gray-100 p-4 rounded-lg w-80">
      <h3 className="font-bold mb-4">{title}</h3>
      <SortableContext items={cards.map(c => c.id)} strategy={verticalListSortingStrategy}>
        <div className="space-y-2">
          {cards.map(card => <SortableCard key={card.id} {...card} />)}
        </div>
      </SortableContext>
    </div>
  );
}

function KanbanBoard() {
  const [columns, setColumns] = useState(initialData);

  const handleDragEnd = (event) => {
    const { active, over } = event;
    if (!over || active.id === over.id) return;
    // Move card between columns or reorder within column
    setColumns(prev => moveCard(prev, active.id, over.id));
  };

  return (
    <DndContext collisionDetection={closestCorners} onDragEnd={handleDragEnd}>
      <div className="flex gap-6 p-8">
        {columns.map(col => <Column key={col.id} {...col} />)}
      </div>
    </DndContext>
  );
}

⚠️ Don’t Do This

❌ Using the HTML5 Drag and Drop API directly

// Inconsistent across browsers, no touch support, ugly drag preview
element.setAttribute('draggable', true);
element.addEventListener('dragstart', (e) => {
  e.dataTransfer.setData('text/plain', id);
});

✅ Use @dnd-kit — consistent, accessible, touch-friendly

// Works on desktop and mobile, keyboard accessible, smooth animations
const { setNodeRef, transform } = useSortable({ id });
// Automatically handles all browser inconsistencies

Testing

Verify your implementation with these tests:

// __tests__/drag-drop-kanban-board.test.ts
import { describe, it, expect } from 'vitest';

describe('Drag & Drop Kanban Board', () => {
  it('should initialize without errors', () => {
    // Test that the setup completes successfully
    expect(() => setup()).not.toThrow();
  });

  it('should handle the primary use case', async () => {
    const result = await execute();
    expect(result).toBeDefined();
    expect(result.success).toBe(true);
  });

  it('should handle edge cases', async () => {
    // Test with empty/null input
    const result = await execute(null);
    expect(result.error).toBeDefined();
  });
});

Verification

# Drag a card from 'To Do' to 'In Progress'
# Verify card appears in new column
# Try keyboard: Tab to a card, Space to pick up, arrows to move
# Refresh page — if state is persisted, layout should remain

Related Specs

Beginner

React Form Validation (React Hook Form + Zod)

Type-safe forms, custom validators, error display, and submission handling

Frontend Patterns
Intermediate

Data Fetching with TanStack Query

Queries, mutations, pagination, infinite scroll, and optimistic updates

Frontend Patterns
Intermediate

Next.js Server Actions Guide

Form mutations, revalidation, optimistic updates, and error handling

Frontend Patterns