Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to build a Kanban board in React

A Kanban board organizes tasks into columns representing workflow stages, with drag-and-drop for moving cards between them. As the creator of CoreUI with over 10 years of React experience since 2014, I’ve built Kanban-style interfaces for project management tools, sprint planners, and support ticket systems. The most effective approach stores board state as a map of column IDs to card arrays, uses HTML5 drag-and-drop for movement, and rerenders on every drop. This provides a functional board without heavy dependencies.

Create the board structure with columns and draggable cards.

import { useState } from 'react'

const INITIAL_BOARD = {
  todo: {
    title: 'To Do',
    cards: [
      { id: 'c1', text: 'Design mockups' },
      { id: 'c2', text: 'Write tests' }
    ]
  },
  inProgress: {
    title: 'In Progress',
    cards: [
      { id: 'c3', text: 'Implement API' }
    ]
  },
  done: {
    title: 'Done',
    cards: [
      { id: 'c4', text: 'Set up project' }
    ]
  }
}

function KanbanBoard() {
  const [board, setBoard] = useState(INITIAL_BOARD)
  const [dragging, setDragging] = useState(null)

  const onDragStart = (cardId, fromColumn) => {
    setDragging({ cardId, fromColumn })
  }

  const onDrop = (toColumn) => {
    if (!dragging || dragging.fromColumn === toColumn) return

    const { cardId, fromColumn } = dragging
    const card = board[fromColumn].cards.find(c => c.id === cardId)

    setBoard(prev => ({
      ...prev,
      [fromColumn]: {
        ...prev[fromColumn],
        cards: prev[fromColumn].cards.filter(c => c.id !== cardId)
      },
      [toColumn]: {
        ...prev[toColumn],
        cards: [...prev[toColumn].cards, card]
      }
    }))
    setDragging(null)
  }

  return (
    <div style={{ display: 'flex', gap: '1rem' }}>
      {Object.entries(board).map(([colId, col]) => (
        <KanbanColumn
          key={colId}
          colId={colId}
          column={col}
          onDragStart={onDragStart}
          onDrop={onDrop}
        />
      ))}
    </div>
  )
}

The board state maps column IDs to column objects containing title and cards array. dragging tracks the card being moved and its source column. On drop, the card is removed from the source and added to the destination.

Building the Column Component

Handle drop events and render cards.

function KanbanColumn({ colId, column, onDragStart, onDrop }) {
  const [isOver, setIsOver] = useState(false)

  return (
    <div
      onDragOver={(e) => { e.preventDefault(); setIsOver(true) }}
      onDragLeave={() => setIsOver(false)}
      onDrop={() => { onDrop(colId); setIsOver(false) }}
      style={{
        width: 280,
        minHeight: 400,
        background: isOver ? '#e8f4fd' : '#f4f5f7',
        borderRadius: 8,
        padding: '1rem',
        border: isOver ? '2px dashed #3498db' : '2px solid transparent'
      }}
    >
      <h3>{column.title} <span>({column.cards.length})</span></h3>
      {column.cards.map(card => (
        <KanbanCard
          key={card.id}
          card={card}
          colId={colId}
          onDragStart={onDragStart}
        />
      ))}
    </div>
  )
}

onDragOver must call preventDefault() to allow dropping. isOver highlights the column during drag. The card count shows in the header. The visual feedback makes it clear where the card will land.

Building the Card Component

Make cards draggable.

function KanbanCard({ card, colId, onDragStart }) {
  return (
    <div
      draggable
      onDragStart={() => onDragStart(card.id, colId)}
      style={{
        background: 'white',
        borderRadius: 4,
        padding: '0.75rem',
        marginBottom: '0.5rem',
        boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
        cursor: 'grab'
      }}
    >
      {card.text}
    </div>
  )
}

The draggable attribute enables HTML5 drag-and-drop. onDragStart records the card and source column. The grab cursor signals the card is draggable.

Adding New Cards

Allow users to add tasks to columns.

function KanbanColumn({ colId, column, onDragStart, onDrop, onAddCard }) {
  const [adding, setAdding] = useState(false)
  const [text, setText] = useState('')

  const handleAdd = () => {
    if (text.trim()) {
      onAddCard(colId, text.trim())
      setText('')
      setAdding(false)
    }
  }

  return (
    <div /* ...drag props */ >
      <h3>{column.title}</h3>
      {column.cards.map(card => <KanbanCard key={card.id} card={card} colId={colId} onDragStart={onDragStart} />)}
      {adding ? (
        <div>
          <textarea value={text} onChange={e => setText(e.target.value)} autoFocus />
          <button onClick={handleAdd}>Add</button>
          <button onClick={() => setAdding(false)}>Cancel</button>
        </div>
      ) : (
        <button onClick={() => setAdding(true)}>+ Add card</button>
      )}
    </div>
  )
}
// In KanbanBoard
const onAddCard = (colId, text) => {
  const newCard = { id: `c${Date.now()}`, text }
  setBoard(prev => ({
    ...prev,
    [colId]: {
      ...prev[colId],
      cards: [...prev[colId].cards, newCard]
    }
  }))
}

Each column has an inline “Add card” form. The form appears on click and hides on cancel or submit. Unique IDs use timestamps. The board state updates immutably.

Best Practice Note

This is the same Kanban architecture we use in CoreUI project management templates. For production apps, replace HTML5 drag-and-drop with @dnd-kit/core for better touch support, accessibility, and smoother animations. Persist board state to localStorage or a backend API. Add card editing, labels, due dates, and assignments for richer project management. Consider card ordering within columns using index-based logic for precise positioning. For multi-user boards, implement optimistic updates and WebSocket sync to reflect other users’ changes in real time.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team