Skip to main content

Board Style

Card Style

Shadow

Column Width

280px

Column Gap

16px

Card Gap

12px

Features

Card Content

Colors

Themes

Preview

To Do

3

Design homepage wireframes

Write API documentation

Set up CI/CD pipeline

In Progress

2

Implement user authentication

Create dashboard components

In Review

1

Database schema design

Done

2

Project setup and configuration

Design system documentation

HTML
<div class="kanban-board">
  <div class="kanban-column">
    <div class="column-header">
      <h3 class="column-title">To Do</h3>
      <span class="card-count">3</span>
    </div>
    <div class="column-content">
      <div class="kanban-card" draggable="true">
        <div class="card-labels">
          <span class="card-label" style="background: #61bd4f">Design</span>
          <span class="card-label" style="background: #eb5a46">High Priority</span>
        </div>
        <h4 class="card-title">Design homepage wireframes</h4>
        <div class="card-footer">
          <span class="card-due-date">Jan 15</span>
          <span class="card-assignee">JD</span>
        </div>
      </div>
      <!-- More cards... -->
    </div>
    <button class="add-card-btn">+ Add a card</button>
  </div>
  <!-- More columns... -->
</div>
CSS
.kanban-board {
  display: flex;
  gap: 16px;
  padding: 20px;
  background: #f4f5f7;
  min-height: 100vh;
  overflow-x: auto;
}

.kanban-column {
  flex: 0 0 280px;
  background: #ebecf0;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 40px);
}

.column-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  
}

.column-title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: #5e6c84;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.card-count {
  background: #dfe1e6;
  color: #5e6c84;
  font-size: 12px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 10px;
}

.column-content {
  flex: 1;
  padding: 0 8px 8px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.kanban-card {
  background: #ffffff;
  border-radius: 6px;
  padding: 12px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  cursor: grab;
  transition: transform 0.1s ease, box-shadow 0.1s ease;
  
}

.kanban-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}

.kanban-card.dragging {
  opacity: 0.5;
  cursor: grabbing;
}

.card-labels {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 8px;
}

.card-label {
  display: inline-block;
  height: 8px;
  min-width: 32px;
  border-radius: 4px;
  
}

.card-title {
  margin: 0 0 8px;
  font-size: 14px;
  font-weight: 500;
  color: #172b4d;
  line-height: 1.4;
}

.card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 12px;
  color: #5e6c84;
}

.card-due-date {
  display: flex;
  align-items: center;
  gap: 4px;
}

.card-due-date::before {
  content: '📅';
  font-size: 12px;
}

.card-assignee {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: #0079bf;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 600;
}

.add-card-btn {
  width: 100%;
  padding: 10px;
  margin: 8px;
  margin-top: auto;
  background: transparent;
  border: none;
  border-radius: 6px;
  color: #5e6c84;
  font-size: 14px;
  cursor: pointer;
  text-align: left;
  transition: background 0.2s;
}

.add-card-btn:hover {
  background: rgba(0,0,0,0.05);
  color: #172b4d;
}
JavaScript
class KanbanBoard {
  constructor(container, options = {}) {
    this.container = container;
    this.options = {
      draggable: true,
      onCardMove: null,
      onCardAdd: null,
      onCardDelete: null,
      ...options
    };
    this.draggedCard = null;
    this.init();
  }

  init() {
    if (this.options.draggable) {
      this.initDragAndDrop();
    }
    this.initAddCardButtons();
  }

  initDragAndDrop() {
    const cards = this.container.querySelectorAll('.kanban-card');
    const columns = this.container.querySelectorAll('.column-content');

    cards.forEach(card => {
      card.addEventListener('dragstart', (e) => this.handleDragStart(e, card));
      card.addEventListener('dragend', (e) => this.handleDragEnd(e, card));
    });

    columns.forEach(column => {
      column.addEventListener('dragover', (e) => this.handleDragOver(e));
      column.addEventListener('drop', (e) => this.handleDrop(e, column));
      column.addEventListener('dragenter', (e) => this.handleDragEnter(e, column));
      column.addEventListener('dragleave', (e) => this.handleDragLeave(e, column));
    });
  }

  handleDragStart(e, card) {
    this.draggedCard = card;
    card.classList.add('dragging');
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/html', card.outerHTML);
  }

  handleDragEnd(e, card) {
    card.classList.remove('dragging');
    this.draggedCard = null;

    // Remove all drop indicators
    this.container.querySelectorAll('.drop-indicator').forEach(el => el.remove());
    this.container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
  }

  handleDragOver(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
  }

  handleDragEnter(e, column) {
    column.classList.add('drag-over');
  }

  handleDragLeave(e, column) {
    if (!column.contains(e.relatedTarget)) {
      column.classList.remove('drag-over');
    }
  }

  handleDrop(e, column) {
    e.preventDefault();
    column.classList.remove('drag-over');

    if (this.draggedCard) {
      const sourceColumn = this.draggedCard.closest('.column-content');
      const targetColumn = column;

      // Find drop position
      const afterElement = this.getDragAfterElement(column, e.clientY);

      if (afterElement) {
        targetColumn.insertBefore(this.draggedCard, afterElement);
      } else {
        targetColumn.appendChild(this.draggedCard);
      }

      // Update card counts
      this.updateCardCounts();

      // Callback
      if (this.options.onCardMove) {
        this.options.onCardMove({
          cardId: this.draggedCard.dataset.id,
          fromColumn: sourceColumn.closest('.kanban-column').dataset.id,
          toColumn: targetColumn.closest('.kanban-column').dataset.id
        });
      }
    }
  }

  getDragAfterElement(column, y) {
    const cards = [...column.querySelectorAll('.kanban-card:not(.dragging)')];

    return cards.reduce((closest, card) => {
      const box = card.getBoundingClientRect();
      const offset = y - box.top - box.height / 2;

      if (offset < 0 && offset > closest.offset) {
        return { offset: offset, element: card };
      } else {
        return closest;
      }
    }, { offset: Number.NEGATIVE_INFINITY }).element;
  }

  updateCardCounts() {
    this.container.querySelectorAll('.kanban-column').forEach(column => {
      const count = column.querySelectorAll('.kanban-card').length;
      const countEl = column.querySelector('.card-count');
      if (countEl) {
        countEl.textContent = count;
      }
    });
  }

  initAddCardButtons() {
    this.container.querySelectorAll('.add-card-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        const column = btn.closest('.kanban-column');
        this.showAddCardForm(column);
      });
    });
  }

  showAddCardForm(column) {
    // Implement your add card form logic
    const title = prompt('Enter card title:');
    if (title) {
      this.addCard(column, { title });
    }
  }

  addCard(column, cardData) {
    const cardHtml = this.createCardElement(cardData);
    const content = column.querySelector('.column-content');
    content.insertAdjacentHTML('beforeend', cardHtml);

    this.updateCardCounts();

    if (this.options.draggable) {
      this.initDragAndDrop();
    }

    if (this.options.onCardAdd) {
      this.options.onCardAdd({
        columnId: column.dataset.id,
        card: cardData
      });
    }
  }

  createCardElement(cardData) {
    return `
      <div class="kanban-card" draggable="true" data-id="${Date.now()}">
        <h4 class="card-title">${cardData.title}</h4>
      </div>
    `;
  }
}

// Usage
const board = new KanbanBoard(document.querySelector('.kanban-board'), {
  onCardMove: (data) => console.log('Card moved:', data),
  onCardAdd: (data) => console.log('Card added:', data)
});
React Component
import React, { useState, useRef } from 'react';
import './KanbanBoard.css';

const KanbanCard = ({
  card,
  draggable,
  showLabels,
  showAssignee,
  showDueDate,
  onDragStart,
  onDragEnd,
  labelColors
}) => {
  const handleDragStart = (e) => {
    onDragStart(e, card);
    e.currentTarget.classList.add('dragging');
  };

  const handleDragEnd = (e) => {
    onDragEnd(e, card);
    e.currentTarget.classList.remove('dragging');
  };

  return (
    <div
      className="kanban-card"
      draggable={draggable}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
    >
      {showLabels && card.labels && (
        <div className="card-labels">
          {card.labels.map((label, i) => (
            <span
              key={i}
              className="card-label"
              style={{ background: labelColors[label] || '#ccc' }}
            />
          ))}
        </div>
      )}
      <h4 className="card-title">{card.title}</h4>
      <div className="card-footer">
        {showDueDate && card.dueDate && (
          <span className="card-due-date">{card.dueDate}</span>
        )}
        {showAssignee && card.assignee && (
          <span className="card-assignee">{card.assignee}</span>
        )}
      </div>

      <ToolFooter />
    </div>
  );
};

const KanbanColumn = ({
  column,
  draggable,
  showCardCount,
  showAddCard,
  showLabels,
  showAssignee,
  showDueDate,
  onDragStart,
  onDragEnd,
  onDragOver,
  onDrop,
  onAddCard,
  labelColors
}) => {
  const [isDragOver, setIsDragOver] = useState(false);

  const handleDragOver = (e) => {
    e.preventDefault();
    onDragOver(e);
  };

  const handleDrop = (e) => {
    e.preventDefault();
    setIsDragOver(false);
    onDrop(e, column.id);
  };

  return (
    <div className="kanban-column">
      <div className="column-header">
        <h3 className="column-title">{column.title}</h3>
        {showCardCount && (
          <span className="card-count">{column.cards.length}</span>
        )}
      </div>
      <div
        className={`column-content ${isDragOver ? 'drag-over' : ''}`}
        onDragOver={handleDragOver}
        onDrop={handleDrop}
        onDragEnter={() => setIsDragOver(true)}
        onDragLeave={(e) => {
          if (!e.currentTarget.contains(e.relatedTarget)) {
            setIsDragOver(false);
          }
        }}
      >
        {column.cards.map(card => (
          <KanbanCard
            key={card.id}
            card={card}
            draggable={draggable}
            showLabels={showLabels}
            showAssignee={showAssignee}
            showDueDate={showDueDate}
            onDragStart={onDragStart}
            onDragEnd={onDragEnd}
            labelColors={labelColors}
          />
        ))}
      </div>
      {showAddCard && (
        <button
          className="add-card-btn"
          onClick={() => onAddCard(column.id)}
        >
          + Add a card
        </button>
      )}
    </div>
  );
};

const KanbanBoard = ({
  initialColumns = [],
  draggable = true,
  showCardCount = true,
  showAddCard = true,
  showLabels = true,
  showAssignee = true,
  showDueDate = true,
  labelColors = {},
  onCardMove,
  onCardAdd
}) => {
  const [columns, setColumns] = useState(initialColumns);
  const draggedCard = useRef(null);
  const sourceColumn = useRef(null);

  const handleDragStart = (e, card) => {
    draggedCard.current = card;
    sourceColumn.current = columns.find(col =>
      col.cards.some(c => c.id === card.id)
    )?.id;
    e.dataTransfer.effectAllowed = 'move';
  };

  const handleDragEnd = () => {
    draggedCard.current = null;
    sourceColumn.current = null;
  };

  const handleDragOver = (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
  };

  const handleDrop = (e, targetColumnId) => {
    if (!draggedCard.current || sourceColumn.current === targetColumnId) return;

    const card = draggedCard.current;
    const fromColumnId = sourceColumn.current;

    setColumns(prev => prev.map(col => {
      if (col.id === fromColumnId) {
        return {
          ...col,
          cards: col.cards.filter(c => c.id !== card.id)
        };
      }
      if (col.id === targetColumnId) {
        return {
          ...col,
          cards: [...col.cards, card]
        };
      }
      return col;
    }));

    onCardMove?.({
      card,
      fromColumn: fromColumnId,
      toColumn: targetColumnId
    });
  };

  const handleAddCard = (columnId) => {
    const title = prompt('Enter card title:');
    if (!title) return;

    const newCard = {
      id: Date.now(),
      title,
      labels: [],
      assignee: null,
      dueDate: null
    };

    setColumns(prev => prev.map(col => {
      if (col.id === columnId) {
        return {
          ...col,
          cards: [...col.cards, newCard]
        };
      }
      return col;
    }));

    onCardAdd?.({ columnId, card: newCard });
  };

  return (
    <div className="kanban-board">
      {columns.map(column => (
        <KanbanColumn
          key={column.id}
          column={column}
          draggable={draggable}
          showCardCount={showCardCount}
          showAddCard={showAddCard}
          showLabels={showLabels}
          showAssignee={showAssignee}
          showDueDate={showDueDate}
          onDragStart={handleDragStart}
          onDragEnd={handleDragEnd}
          onDragOver={handleDragOver}
          onDrop={handleDrop}
          onAddCard={handleAddCard}
          labelColors={labelColors}
        />
      ))}
    </div>
  );
};

export default KanbanBoard;