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.



