Back to Projects Expert

Kanban Board

Build a drag-and-drop Kanban board with three columns - To Do, In Progress, and Done. Master the HTML5 Drag and Drop API, dynamic DOM manipulation, and localStorage persistence.

HTML, CSS, JavaScript ~2-3 hours to build Free source code

What You’ll Learn

  • How to use the HTML5 Drag and Drop API (dragstart, dragover, drop events)
  • How to create, move, and delete DOM elements dynamically
  • How to persist board state to localStorage as JSON
  • How to restore saved data and re-render cards on page load
  • How to add visual feedback during drag operations with CSS class toggling

How It Works

dragstart marks the card being moved

When a drag begins, we store the card element’s unique ID in dataTransfer.setData(). This is the browser’s built-in way to pass information from the source element to the drop target — even across columns.

dragover enables the drop zone

By default, the browser prevents drops. We call event.preventDefault() inside the dragover handler on each column to opt in. We also add a CSS highlight class so the user sees where the card will land.

drop moves the card in the DOM

Inside the drop handler, we read the card ID from dataTransfer.getData(), find that DOM element, and appendChild() it into the target column. The card physically moves in the DOM — no copy, no clone.

State is serialized to localStorage

After every action (add, move, delete), we read which column each card belongs to and save the full board state as a JSON string. On load, we parse that string and rebuild every card from scratch.

Source Code

HTML Structure

HTML
<!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>
  <div class="app">
    <h1>Kanban Board</h1>
    <div class="board">
      <div class="column" id="todo" data-col="todo">
        <div class="col-header">
          <span class="col-dot dot-todo"></span>
          <h2>To Do</h2>
          <span class="card-count" id="count-todo">0</span>
        </div>
        <div class="cards" id="cards-todo"></div>
        <button class="add-btn" data-col="todo">+ Add Card</button>
      </div>
      <div class="column" id="inprogress" data-col="inprogress">
        <div class="col-header">
          <span class="col-dot dot-inprogress"></span>
          <h2>In Progress</h2>
          <span class="card-count" id="count-inprogress">0</span>
        </div>
        <div class="cards" id="cards-inprogress"></div>
        <button class="add-btn" data-col="inprogress">+ Add Card</button>
      </div>
      <div class="column" id="done" data-col="done">
        <div class="col-header">
          <span class="col-dot dot-done"></span>
          <h2>Done</h2>
          <span class="card-count" id="count-done">0</span>
        </div>
        <div class="cards" id="cards-done"></div>
        <button class="add-btn" data-col="done">+ Add Card</button>
      </div>
    </div>
  </div>
  <script src="script.js"></script>
</body>
</html>

CSS Styling

CSS
* { margin:0; padding:0; box-sizing:border-box; }
body { min-height:100vh; background:linear-gradient(135deg,#1a1a2e,#3d1a00);
  font-family:'Segoe UI',sans-serif; padding:24px 12px; }
.app { max-width:960px; margin:0 auto; }
h1 { color:#fff; font-size:1.6rem; font-weight:800; text-align:center;
  margin-bottom:24px; letter-spacing:-.5px; }
.board { display:flex; gap:16px; align-items:flex-start; }
.column { flex:1; background:#fff; border-radius:16px; padding:16px;
  box-shadow:0 8px 30px rgba(0,0,0,0.25); min-height:400px;
  display:flex; flex-direction:column; transition:background .2s; }
.column.drag-over { background:#fff8f0; }
.col-header { display:flex; align-items:center; gap:8px; margin-bottom:14px; }
.col-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
.dot-todo { background:#e67e22; }
.dot-inprogress { background:#3498db; }
.dot-done { background:#27ae60; }
.col-header h2 { font-size:.9rem; font-weight:700; color:#333; flex:1;
  text-transform:uppercase; letter-spacing:.5px; }
.card-count { background:#f0f0f0; color:#666; font-size:.72rem; font-weight:700;
  padding:2px 8px; border-radius:20px; }
.cards { flex:1; min-height:120px; display:flex; flex-direction:column; gap:8px; }
.card { background:#f8f9fa; border:1px solid #e8e8e8; border-radius:10px;
  padding:12px 14px; font-size:.88rem; color:#333; cursor:grab; line-height:1.5;
  display:flex; justify-content:space-between; align-items:flex-start;
  gap:8px; transition:box-shadow .2s, opacity .2s; }
.card:hover { box-shadow:0 4px 12px rgba(0,0,0,0.1); }
.card.dragging { opacity:.4; cursor:grabbing; }
.card-text { flex:1; }
.del-btn { background:none; border:none; color:#bbb; font-size:1rem;
  cursor:pointer; line-height:1; padding:0 2px; flex-shrink:0; }
.del-btn:hover { color:#e53e3e; }
.add-btn { margin-top:12px; width:100%; padding:8px; background:transparent;
  border:2px dashed #ddd; border-radius:10px; color:#aaa; font-size:.82rem;
  font-weight:600; cursor:pointer; transition:all .2s; }
.add-btn:hover { border-color:#e67e22; color:#e67e22; background:#fff8f0; }
@media(max-width:640px) { .board { flex-direction:column; } }
.add-form { display:flex; gap:6px; margin-top:12px; }
.add-form input { flex:1; padding:7px 10px; border:1px solid #ddd; border-radius:8px; font-size:.85rem; outline:none; }
.add-form input:focus { border-color:#e67e22; }
.confirm-btn { padding:7px 12px; background:#e67e22; border:none; border-radius:8px; color:#fff; font-size:.85rem; font-weight:600; cursor:pointer; }
.cancel-btn { padding:7px 10px; background:#f0f0f0; border:none; border-radius:8px; color:#666; font-size:.85rem; cursor:pointer; }

JavaScript Logic

JavaScript
const COLS = ['todo', 'inprogress', 'done'];
let dragId = null;

function uid() {
  return Date.now().toString(36) + Math.random().toString(36).slice(2);
}

function saveBoard() {
  const state = {};
  COLS.forEach(col => {
    state[col] = [...document.querySelectorAll(`#cards-${col} .card`)].map(c => ({
      id: c.dataset.id,
      text: c.querySelector('.card-text').textContent
    }));
  });
  localStorage.setItem('kanban-board', JSON.stringify(state));
}

function updateCounts() {
  COLS.forEach(col => {
    document.getElementById(`count-${col}`).textContent =
      document.querySelectorAll(`#cards-${col} .card`).length;
  });
}

function createCard(id, text) {
  const card = document.createElement('div');
  card.className = 'card';
  card.draggable = true;
  card.dataset.id = id;
  card.innerHTML = `<span class="card-text">${text}</span><button class="del-btn" title="Delete">&times;</button>`;

  card.addEventListener('dragstart', e => {
    dragId = id;
    setTimeout(() => card.classList.add('dragging'), 0);
  });
  card.addEventListener('dragend', () => {
    card.classList.remove('dragging');
    saveBoard();
    updateCounts();
  });
  card.querySelector('.del-btn').addEventListener('click', () => {
    card.remove();
    saveBoard();
    updateCounts();
  });
  return card;
}

function addCard(col) {
  const colEl = document.getElementById(col);
  const existing = colEl.querySelector('.add-form');
  if (existing) { existing.querySelector('input').focus(); return; }
  const form = document.createElement('div');
  form.className = 'add-form';
  form.innerHTML = '<input type="text" placeholder="Card text..." maxlength="100"><button class="confirm-btn">Add</button><button class="cancel-btn">&times;</button>';
  const btn = colEl.querySelector(`.add-btn[data-col="${col}"]`);
  colEl.insertBefore(form, btn);
  const input = form.querySelector('input');
  input.focus();
  function submit() {
    const text = input.value.trim();
    form.remove();
    if (!text) return;
    const card = createCard(uid(), text);
    document.getElementById(`cards-${col}`).appendChild(card);
    saveBoard();
    updateCounts();
  }
  form.querySelector('.confirm-btn').addEventListener('click', submit);
  form.querySelector('.cancel-btn').addEventListener('click', () => form.remove());
  input.addEventListener('keydown', e => {
    if (e.key === 'Enter') submit();
    if (e.key === 'Escape') form.remove();
  });
}

COLS.forEach(col => {
  const cardsEl = document.getElementById(`cards-${col}`);
  const colEl = document.getElementById(col);

  colEl.addEventListener('dragover', e => {
    e.preventDefault();
    colEl.classList.add('drag-over');
  });
  colEl.addEventListener('dragleave', () => colEl.classList.remove('drag-over'));
  colEl.addEventListener('drop', e => {
    e.preventDefault();
    colEl.classList.remove('drag-over');
    if (!dragId) return;
    const card = document.querySelector(`.card[data-id="${dragId}"]`);
    if (card) cardsEl.appendChild(card);
    dragId = null;
    saveBoard();
    updateCounts();
  });
  document.querySelector(`.add-btn[data-col="${col}"]`)
    .addEventListener('click', () => addCard(col));
});

function loadBoard() {
  const saved = localStorage.getItem('kanban-board');
  if (saved) {
    const state = JSON.parse(saved);
    COLS.forEach(col => {
      (state[col] || []).forEach(({ id, text }) => {
        document.getElementById(`cards-${col}`).appendChild(createCard(id, text));
      });
    });
  } else {
    const defaults = { todo: ['Design the layout', 'Set up project files'],
      inprogress: ['Build the card component'], done: ['Plan the features'] };
    COLS.forEach(col => defaults[col].forEach(t =>
      document.getElementById(`cards-${col}`).appendChild(createCard(uid(), t))));
  }
  updateCounts();
}

loadBoard();

Download Source Code

Single ready-to-run HTML file. Open it in any browser!

Download Project

Single HTML file · All code included