Svelte Kanban Board
A fully responsive, drag-and-drop Kanban board component built with Svelte 5 and Tailwind CSS v4. Features vertical stacking on mobile, grid layout on larger screens, customizable column colors, card tags, and accessible ARIA labels. Styles are centralized using tailwind-variants for easy theming and maintainability.
Setup #
- Svelte
<script lang="ts">
import { type KanbanCardType, type KanbanColumnType, KanbanBoard } from 'flowbite-svelte';
</script>Default Kanban Board #
Pass columns array with bindable prop, handle onMove and onAddCard callbacks. Supports drag-and-drop between columns with visual feedback.
- Svelte
<script lang="ts">
import { type KanbanCardType, type KanbanColumnType, KanbanBoard } from "flowbite-svelte";
let columns = $state<KanbanColumnType[]>([
{
id: "todo",
title: "To Do",
color: "#ef4444",
cards: [
{
id: 1,
title: "Design new landing page",
description: "Create mockups for the homepage redesign",
tags: ["design", "urgent"]
},
{
id: 2,
title: "Update documentation",
description: "Add API examples to the docs",
tags: ["docs"]
}
]
},
{
id: "in-progress",
title: "In Progress",
color: "#f59e0b",
cards: [
{
id: 3,
title: "Implement authentication",
description: "Add JWT-based auth system",
tags: ["backend", "security"]
}
]
},
{
id: "review",
title: "Review",
color: "#8b5cf6",
cards: [
{
id: 4,
title: "Code review: Payment flow",
tags: ["review"]
}
]
},
{
id: "done",
title: "Done",
color: "#10b981",
cards: [
{
id: 5,
title: "Setup CI/CD pipeline",
description: "Configure GitHub Actions",
tags: ["devops"]
},
{
id: 6,
title: "Database migration",
tags: ["backend", "completed"]
}
]
}
]);
function handleMove(card: KanbanCardType, from: KanbanColumnType, to: KanbanColumnType) {
console.log(`Moved "${card.title}" from "${from.title}" to "${to.title}"`);
// The KanbanBoard component already mutated the columns.
// We just need to trigger reactivity by creating a new reference
columns = [...columns];
// Here you could make an API call to persist the change
// await fetch('/api/cards/move', { method: 'POST', body: JSON.stringify({ cardId: card.id, fromId: from.id, toId: to.id }) })
}
function handleAddCard(col: KanbanColumnType) {
// Note: Using prompt() for demo - use proper form UI in production
const cardTitle = prompt(`Add a new card to "${col.title}":`);
if (!cardTitle?.trim()) return;
const newCard: KanbanCardType = {
// Note: Using timestamp for demo - use proper ID generation in production
id: Date.now(),
title: cardTitle.trim(),
tags: ["new"]
};
columns = columns.map((column) => (column.id === col.id ? { ...column, cards: [...column.cards, newCard] } : column));
// Here you could make an API call to persist the new card
// await fetch('/api/cards', { method: 'POST', body: JSON.stringify(newCard) })
}
</script>
<div class="bg-gray-100 py-4 md:py-8">
<div class="mx-auto max-w-7xl px-2 sm:px-4">
<div class="mb-4 md:mb-6">
<h1 class="text-2xl font-bold text-gray-900 md:text-3xl">Project Kanban Board</h1>
<p class="mt-1 text-sm text-gray-600 md:mt-2 md:text-base">Drag cards between columns to update their status</p>
</div>
<KanbanBoard bind:columns onMove={handleMove} onAddCard={handleAddCard} />
<!-- Optional: Show stats -->
<div class="mt-6 grid grid-cols-2 gap-3 md:mt-8 md:grid-cols-4 md:gap-4">
{#each columns as col (col.id)}
<div class="rounded-lg bg-white p-3 shadow-sm md:p-4">
<div class="text-xs text-gray-600 md:text-sm">{col.title}</div>
<div class="mt-1 text-xl font-bold text-gray-900 md:text-2xl">{col.cards.length}</div>
</div>
{/each}
</div>
</div>
</div>Custom Styling #
- Svelte
<script lang="ts">
import { type KanbanColumnType, KanbanBoard } from "flowbite-svelte";
let columns = $state<KanbanColumnType[]>([
{
id: "backlog",
title: "Backlog",
color: "#6b7280",
cards: [{ id: 1, title: "Research user feedback", tags: ["research"] }]
},
{
id: "active",
title: "Active Sprint",
color: "#3b82f6",
cards: [{ id: 2, title: "Build feature X", description: "Sprint 24", tags: ["dev", "high-priority"] }]
},
{
id: "done",
title: "Completed",
color: "#22c55e",
cards: []
}
]);
function handleMove() {
columns = [...columns];
}
function handleAddCard(col: KanbanColumnType) {
const title = prompt(`Add card to ${col.title}:`);
if (!title?.trim()) return;
columns = columns.map((column) => (column.id === col.id ? { ...column, cards: [...column.cards, { id: crypto.randomUUID(), title: title.trim() }] } : column));
}
</script>
<KanbanBoard
bind:columns
onMove={handleMove}
onAddCard={handleAddCard}
classes={{
column: "bg-white shadow-lg",
card: "hover:shadow-xl transition-shadow",
cardTitle: "text-blue-600 font-bold",
addButton: "bg-blue-500 hover:bg-blue-600 text-white"
}}
/>Using LocalStorage #
- Svelte
<script lang="ts">
import { type KanbanCardType, type KanbanColumnType, KanbanBoard } from "flowbite-svelte";
import { onMount } from "svelte";
const STORAGE_KEY = "my-kanban-board";
let columns = $state<KanbanColumnType[]>([
{ id: "todo", title: "To Do", color: "#ef4444", cards: [] },
{ id: "doing", title: "Doing", color: "#f59e0b", cards: [] },
{ id: "done", title: "Done", color: "#10b981", cards: [] }
]);
// Load from localStorage on mount
onMount(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
columns = JSON.parse(saved);
} catch (e) {
console.error("Failed to load saved board:", e);
}
}
});
// Save to localStorage whenever columns change
$effect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(columns));
});
function handleMove(card: KanbanCardType, from: KanbanColumnType, to: KanbanColumnType) {
console.log(`Moved "${card.title}" from "${from.title}" to "${to.title}"`);
columns = [...columns]; // Trigger reactivity and persistence (card already moved by component)
}
function handleAddCard(col: KanbanColumnType) {
const title = prompt(`New task for ${col.title}:`);
if (!title?.trim()) return;
columns = columns.map((column) =>
column.id === col.id
? {
...column,
cards: [
...column.cards,
{
id: Date.now(),
title: title.trim(),
tags: ["new"]
}
]
}
: column
);
}
function clearBoard() {
if (confirm("Clear all cards? This cannot be undone.")) {
columns = columns.map((col) => ({ ...col, cards: [] }));
}
}
</script>
<div class="p-4">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">My Tasks</h1>
<button onclick={clearBoard} class="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600">Clear Board</button>
</div>
<KanbanBoard bind:columns onMove={handleMove} onAddCard={handleAddCard} />
</div>With API Integration (Demo Only) #
⚠️ Note: The API endpoints in this example use simple in-memory storage for demonstration purposes. This is not suitable for production as it has limitations:
- Data resets on server restart
- Not safe for concurrent requests
- Doesn’t scale across multiple server instances
For production, use a proper database (PostgreSQL, MongoDB, Supabase, etc.) with proper transaction handling.
- Svelte
<script lang="ts">
import { type KanbanCardType, type KanbanColumnType, KanbanBoard } from "flowbite-svelte";
import { onMount } from "svelte";
let columns = $state<KanbanColumnType[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
onMount(async () => {
try {
const response = await fetch("/api/kanban/columns");
if (!response.ok) throw new Error("Failed to load board");
columns = await response.json();
} catch (e) {
error = e instanceof Error ? e.message : "Unknown error";
} finally {
loading = false;
}
});
async function handleMove(card: KanbanCardType, from: KanbanColumnType, to: KanbanColumnType) {
// Trigger reactivity to reflect the card move performed by KanbanBoard
columns = [...columns];
try {
const response = await fetch("/api/kanban/move", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
cardId: card.id,
fromColumnId: from.id,
toColumnId: to.id
})
});
if (!response.ok) throw new Error("Failed to move card");
} catch (e) {
// Rollback on error
alert("Failed to move card. Please try again.");
// Reload from server
const response = await fetch("/api/kanban/columns");
columns = await response.json();
}
}
async function handleAddCard(col: KanbanColumnType) {
const title = prompt(`Add card to ${col.title}:`);
if (!title?.trim()) return;
try {
const response = await fetch("/api/kanban/cards", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: title.trim(),
columnId: col.id
})
});
if (!response.ok) throw new Error("Failed to create card");
const newCard = await response.json();
columns = columns.map((column) => (column.id === col.id ? { ...column, cards: [...column.cards, newCard] } : column));
} catch (e) {
alert("Failed to create card. Please try again.");
}
}
</script>
{#if loading}
<div class="flex h-64 items-center justify-center">
<div class="text-gray-600">Loading board...</div>
</div>
{:else if error}
<div class="rounded border border-red-200 bg-red-50 p-4 text-red-800">
Error: {error}
</div>
{:else}
<KanbanBoard bind:columns onMove={handleMove} onAddCard={handleAddCard} />
{/if}