Freedom Sale
Independence Day Special — Unlock the AI Path 70% off our most popular AI course · Limited time offer
--Days
--Hrs
--Min
--Sec
Claim Your Discount
✦ Intermediate ⏱ 45 min

🗂️ Build a Kanban Board with Drag and Drop

🎯 What You'll Build

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.

📋 What You'll Need

1

Build the HTML structure

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>
2

Write the drag-and-drop JavaScript

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();
  }
}
3

Add new cards and delete existing ones

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();
4

Style the board with CSS

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; } }
💡 Tip: To allow reordering cards within a column, check where the mouse is relative to existing cards during dragover, and insert the dragged card above or below the hovered card using insertBefore().

🎉 You Did It!

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.

Found something wrong?

Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.