A Trello-style Kanban board with To Do, In Progress, and Done columns — drag cards between columns, add new cards, and save everything to localStorage.
Three columns with a drop zone, a heading, and an add-card button each.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Board</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<?php require_once __DIR__ . '/../promo_banner.php'; ?>
<header>
<h1>🗂️ Kanban Board</h1>
</header>
<div class="board">
<div class="column" data-column="todo">
<div class="column-header">
<span class="column-dot todo-dot"></span>
<h2>To Do</h2>
<span class="card-count" id="count-todo">0</span>
</div>
<div class="cards" id="col-todo" ondragover="onDragOver(event)" ondrop="onDrop(event, 'todo')"></div>
<button class="add-card-btn" onclick="addCard('todo')">+ Add card</button>
</div>
<div class="column" data-column="inprogress">
<div class="column-header">
<span class="column-dot inprogress-dot"></span>
<h2>In Progress</h2>
<span class="card-count" id="count-inprogress">0</span>
</div>
<div class="cards" id="col-inprogress" ondragover="onDragOver(event)" ondrop="onDrop(event, 'inprogress')"></div>
<button class="add-card-btn" onclick="addCard('inprogress')">+ Add card</button>
</div>
<div class="column" data-column="done">
<div class="column-header">
<span class="column-dot done-dot"></span>
<h2>Done</h2>
<span class="card-count" id="count-done">0</span>
</div>
<div class="cards" id="col-done" ondragover="onDragOver(event)" ondrop="onDrop(event, 'done')"></div>
<button class="add-card-btn" onclick="addCard('done')">+ Add card</button>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
The HTML5 Drag and Drop API fires four events: dragstart, dragover, drop, and dragend. That is all you need.
// app.js
let draggedCard = null;
// Board state: {id, text, column}
let cards = JSON.parse(localStorage.getItem('kanban-cards') || '[]');
// Default starter cards if empty
if (cards.length === 0) {
cards = [
{ id: 1, text: 'Design the homepage layout', column: 'todo' },
{ id: 2, text: 'Write the hero section copy', column: 'todo' },
{ id: 3, text: 'Build the navigation bar', column: 'inprogress' },
{ id: 4, text: 'Set up the project repo', column: 'done' },
];
}
function save() {
localStorage.setItem('kanban-cards', JSON.stringify(cards));
}
function render() {
['todo', 'inprogress', 'done'].forEach(col => {
const container = document.getElementById(`col-${col}`);
container.innerHTML = '';
const colCards = cards.filter(c => c.column === col);
document.getElementById(`count-${col}`).textContent = colCards.length;
colCards.forEach(card => {
const div = document.createElement('div');
div.className = 'card';
div.draggable = true;
div.dataset.id = card.id;
div.innerHTML = `
<span class="card-text">${card.text}</span>
<button class="delete-btn" onclick="deleteCard(${card.id})">✕</button>
`;
div.addEventListener('dragstart', onDragStart);
div.addEventListener('dragend', onDragEnd);
container.appendChild(div);
});
});
}
function onDragStart(e) {
draggedCard = e.currentTarget;
draggedCard.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedCard.dataset.id);
}
function onDragEnd(e) {
if (draggedCard) draggedCard.classList.remove('dragging');
draggedCard = null;
document.querySelectorAll('.cards').forEach(c => c.classList.remove('drag-over'));
}
function onDragOver(e) {
e.preventDefault();
e.currentTarget.classList.add('drag-over');
}
function onDrop(e, targetColumn) {
e.preventDefault();
e.currentTarget.classList.remove('drag-over');
const id = parseInt(e.dataTransfer.getData('text/plain'));
const card = cards.find(c => c.id === id);
if (card) {
card.column = targetColumn;
save();
render();
}
}
Prompt the user for card text, add it to the board, and persist it.
function addCard(column) {
const text = prompt('Card title:');
if (!text || !text.trim()) return;
cards.push({ id: Date.now(), text: text.trim(), column });
save();
render();
}
function deleteCard(id) {
cards = cards.filter(c => c.id !== id);
save();
render();
}
// Initial render on page load
render();
Three equal columns, card hover effects, and a visual highlight when dragging over a column.
/* style.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: #0f172a; color: #fff; min-height: 100vh; }
header { padding: 24px 32px; background: #1e293b; border-bottom: 1px solid #334155; }
header h1 { font-size: 1.3rem; }
.board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; padding: 28px 32px; min-height: calc(100vh - 75px); }
.column { background: #1e293b; border-radius: 14px; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.column-header { display: flex; align-items: center; gap: 8px; }
.column-header h2 { font-size: 0.95rem; font-weight: 700; flex: 1; }
.column-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.todo-dot { background: #64748b; }
.inprogress-dot { background: #F59C0D; }
.done-dot { background: #22c55e; }
.card-count { background: #334155; color: #94a3b8; font-size: 0.72rem; font-weight: 700; padding: 2px 8px; border-radius: 10px; }
.cards { flex: 1; display: flex; flex-direction: column; gap: 8px; min-height: 80px; border-radius: 8px; transition: background 0.15s; padding: 4px; }
.cards.drag-over { background: rgba(245,156,13,0.1); outline: 2px dashed #F59C0D; }
.card { background: #0f172a; border: 1px solid #334155; border-radius: 10px; padding: 12px 14px; display: flex; align-items: flex-start; gap: 8px; cursor: grab; transition: all 0.15s; }
.card:hover { border-color: #F59C0D; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.card.dragging { opacity: 0.4; transform: rotate(2deg); }
.card-text { flex: 1; font-size: 0.88rem; line-height: 1.5; }
.delete-btn { background: none; border: none; color: #475569; font-size: 0.8rem; cursor: pointer; padding: 2px 4px; border-radius: 4px; flex-shrink: 0; }
.delete-btn:hover { color: #ef4444; background: rgba(239,68,68,0.1); }
.add-card-btn { background: none; border: 1px dashed #334155; color: #64748b; padding: 8px; border-radius: 8px; cursor: pointer; font-size: 0.82rem; width: 100%; transition: all 0.15s; }
.add-card-btn:hover { border-color: #F59C0D; color: #F59C0D; }
@media (max-width: 700px) { .board { grid-template-columns: 1fr; } }
You built drag-and-drop with zero libraries — just 4 native browser events. Cards persist across browser sessions via localStorage. The same HTML5 Drag and Drop API powers file upload drag zones, sortable lists, and desktop-style interfaces.
Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.