Drag and Drop ohne externe Bibliothek
Ein Kanban Board mit Spalten, verschiebbaren Karten und persistentem Zustand lässt sich vollständig mit Alpine.js und der nativen HTML5 Drag and Drop API umsetzen. Kein SortableJS, kein jQuery UI, kein zusätzliches Kilobyte JavaScript, dafür volle Kontrolle über Verhalten, Barrierefreiheit und Darstellung des Boards.
Inhaltsverzeichnis
- 1. Warum ein Kanban Board mit Alpine.js sinnvoll ist
- 2. Grundstruktur: Spalten und Karten als reaktiver State
- 3. Native Drag and Drop API: dragstart, dragover, drop
- 4. Die vollständige Kanban Board Komponente
- 5. Visuelles Feedback: Drop Zonen hervorheben
- 6. Sortierung innerhalb einer Spalte
- 7. Persistenz: Board Zustand im localStorage speichern
- 8. Barrierefreiheit: Tastatur Alternative zum Drag and Drop
- 9. Kanban Board Ansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein Kanban Board mit Alpine.js sinnvoll ist
Ein Kanban Board gehört zu den Komponenten, bei denen Teams reflexartig zu einer schweren Bibliothek greifen. SortableJS, jQuery UI Sortable oder ein komplettes React Drag and Drop Framework wandern ins Bundle, nur um Karten von einer Spalte in die andere zu ziehen. Dabei bringt jeder moderne Browser mit der HTML5 Drag and Drop API bereits alles mit, was ein funktionierendes Kanban Board braucht. Kombiniert mit Alpine.js als reaktiver Zustandsschicht entsteht ein Board, das ohne Build Step, ohne zusätzliches Bundle und ohne Framework Overhead auskommt.
Der Reiz eines selbst gebauten Kanban Board liegt in der Kontrolle. Externe Drag and Drop Bibliotheken bringen eigene Event Zyklen, eigene CSS Klassen und eigene Annahmen über die Datenstruktur mit. Wer ein Kanban Board in ein bestehendes Hyvä Theme oder eine Magento Admin Oberfläche integrieren will, stößt schnell an Grenzen dieser Annahmen. Mit Alpine.js bleibt die gesamte Logik in einer einzigen, lesbaren x-data Komponente, und jede Anpassung ist ein direkter Eingriff in den eigenen Code statt eine Konfigurationsoption einer fremden Bibliothek.
Dieser Artikel baut ein vollständiges Kanban Board von Grund auf: Spalten als Container, Karten als verschiebbare Elemente, native Drag Events für die Interaktion und Alpine.js für den reaktiven Zustand. Am Ende steht ein Kanban Board, das Karten zwischen Spalten verschiebt, innerhalb einer Spalte sortiert, den Zustand im Browser speichert und auch ohne Maus per Tastatur bedienbar bleibt.
2. Grundstruktur: Spalten und Karten als reaktiver State
Die Datenstruktur eines Kanban Board ist im Kern simpel: eine Liste von Spalten, jede Spalte mit einem Titel und einer Liste von Karten. In Alpine.js bildet man das direkt als verschachteltes Array im x-data Objekt ab, ganz ohne separate State Management Bibliothek. Jede Karte bekommt eine eindeutige id, einen Titel und optional weitere Felder wie Priorität oder Zuständigkeit. Diese flache, serialisierbare Struktur ist die Voraussetzung dafür, dass das Kanban Board später problemlos in localStorage oder eine Datenbank geschrieben werden kann.
Wichtig ist, die Spalten selbst als Array von Objekten zu modellieren, nicht als feste Eigenschaften wie todo, doing und done. Mit einem Array aus Spalten Objekten kann das Kanban Board später beliebig viele Spalten unterstützen, ohne den Code anzufassen. Jede Spalte referenziert ihre Karten über eine eigene cards Eigenschaft, und die Render Schleife nutzt zwei verschachtelte x-for Direktiven: die äußere für Spalten, die innere für Karten innerhalb der aktuellen Spalte.
// Kanban Board state: columns as array, cards nested inside each column
function kanbanBoard() {
return {
columns: [
{
id: 'todo',
title: 'Offen',
cards: [
{ id: 1, title: 'Sprint Planung vorbereiten', priority: 'medium' },
{ id: 2, title: 'API Dokumentation aktualisieren', priority: 'low' },
],
},
{
id: 'doing',
title: 'In Arbeit',
cards: [
{ id: 3, title: 'Checkout Bug reproduzieren', priority: 'high' },
],
},
{
id: 'done',
title: 'Fertig',
cards: [],
},
],
// Find the column that currently holds a given card id
findColumnByCardId(cardId) {
return this.columns.find((col) => col.cards.some((c) => c.id === cardId));
},
};
}
Diese Grundstruktur reicht bereits aus, um das Kanban Board statisch zu rendern. Die eigentliche Herausforderung folgt im nächsten Schritt: Karten müssen zwischen den Spalten verschoben werden können, und genau dafür kommt die native Drag and Drop API ins Spiel.
3. Native Drag and Drop API: dragstart, dragover, drop
Die HTML5 Drag and Drop API besteht aus einer Handvoll Events, die zusammen die komplette Interaktion für ein Kanban Board abdecken. dragstart feuert, sobald ein Element mit draggable="true" gezogen wird, und hier speichert man die ID der gezogenen Karte im DataTransfer Objekt des Events. dragover muss auf jeder potenziellen Drop Zone mit event.preventDefault() behandelt werden, sonst verweigert der Browser den Drop kategorisch. Erst drop selbst liest die zuvor gespeicherte Karten ID aus und verschiebt die Karte im Alpine.js State.
Ein häufiger Anfängerfehler beim Kanban Board mit nativer Drag and Drop API: dragover wird ignoriert, weil man annimmt, drop würde automatisch feuern. Tatsächlich verhindert der Browser standardmäßig jeden Drop, außer explizit über preventDefault() im dragover Handler erlaubt. Ein zweiter Stolperstein: dataTransfer.setData() erwartet einen MIME Type String als ersten Parameter, üblicherweise text/plain, und der Wert muss beim Lesen im drop Handler exakt mit demselben Typ wieder abgerufen werden.
// Drag and drop handlers for a Kanban Board card
function kanbanCard(cardId) {
return {
onDragStart(event) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', String(cardId));
// Slight delay so the drag image is captured before we style the source
requestAnimationFrame(() => {
event.target.classList.add('opacity-40');
});
},
onDragEnd(event) {
event.target.classList.remove('opacity-40');
},
};
}
// Drop zone handlers, shared by every column of the Kanban Board
function kanbanDropzone(columnId) {
return {
onDragOver(event) {
event.preventDefault(); // required, or drop never fires
event.dataTransfer.dropEffect = 'move';
},
onDrop(event) {
event.preventDefault();
const cardId = Number(event.dataTransfer.getData('text/plain'));
this.moveCardToColumn(cardId, columnId);
},
};
}
4. Die vollständige Kanban Board Komponente
Mit Grundstruktur und Drag Handlern lässt sich die vollständige Kanban Board Komponente zusammensetzen. Entscheidend ist die Methode moveCardToColumn, die eine Karte aus ihrer aktuellen Spalte entfernt und der Zielspalte hinzufügt. Da Alpine.js auf Proxy basiertem reaktivem State beruht, reicht ein einfaches splice gefolgt von push, damit die Ansicht sich automatisch aktualisiert, ganz ohne manuelles Neu Rendern.
Die Template Seite des Kanban Board nutzt zwei verschachtelte x-for Schleifen und bindet draggable, @dragstart, @dragover und @drop direkt an die entsprechenden HTML Elemente. Wichtig ist, jeder Spalte und jeder Karte ein stabiles :key über x-for mitzugeben, sonst verwechselt Alpine.js beim Neu Rendern Elemente und die Drag Zustände geraten durcheinander.
function kanbanBoard() {
return {
columns: [
{ id: 'todo', title: 'Offen', cards: [{ id: 1, title: 'Sprint Planung', priority: 'medium' }] },
{ id: 'doing', title: 'In Arbeit', cards: [] },
{ id: 'done', title: 'Fertig', cards: [] },
],
draggedCardId: null,
onDragStart(event, cardId) {
this.draggedCardId = cardId;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', String(cardId));
},
onDrop(event, targetColumnId) {
event.preventDefault();
const cardId = Number(event.dataTransfer.getData('text/plain'));
this.moveCardToColumn(cardId, targetColumnId);
this.draggedCardId = null;
},
// Remove card from source column, push into target column
moveCardToColumn(cardId, targetColumnId) {
let movedCard = null;
this.columns.forEach((col) => {
const index = col.cards.findIndex((c) => c.id === cardId);
if (index !== -1) {
movedCard = col.cards.splice(index, 1)[0];
}
});
if (!movedCard) return;
const targetColumn = this.columns.find((col) => col.id === targetColumnId);
targetColumn.cards.push(movedCard);
},
};
}
5. Visuelles Feedback: Drop Zonen hervorheben
Ein Kanban Board ohne visuelles Feedback beim Ziehen fühlt sich unfertig an, selbst wenn die Funktionalität einwandfrei arbeitet. Nutzer erwarten, dass eine Spalte optisch reagiert, sobald eine Karte über sie gezogen wird, ähnlich wie bei Trello oder Jira. Mit Alpine.js lässt sich das über eine einfache dragOverColumn Statusvariable lösen, die beim dragenter Event gesetzt und beim dragleave oder drop wieder zurückgesetzt wird.
Die Krux beim Kanban Board Highlighting: dragleave feuert auch beim Wechsel zwischen Kind Elementen innerhalb derselben Spalte, weil das Event bei jedem Element im DOM Baum ausgelöst wird. Ein zuverlässiger Trick ist, die aktuelle Ziel Spalte anhand einer eindeutigen ID im obersten State zu verfolgen, statt CSS Klassen direkt auf einzelnen Elementen zu toggeln, und die Hervorhebung per :class Binding an genau diese eine Variable zu koppeln.
function kanbanBoard() {
return {
columns: [ /* ... */ ],
dragOverColumnId: null,
onDragEnter(columnId) {
this.dragOverColumnId = columnId;
},
onDragLeaveColumn(event, columnId) {
// Only clear if we actually left the column container, not a child
if (!event.currentTarget.contains(event.relatedTarget)) {
this.dragOverColumnId = null;
}
},
onDrop(event, columnId) {
event.preventDefault();
this.dragOverColumnId = null;
const cardId = Number(event.dataTransfer.getData('text/plain'));
this.moveCardToColumn(cardId, columnId);
},
};
}
Im Template genügt :class="dragOverColumnId === column.id ? 'ring-2 ring-teal-400 bg-teal-50' : ''" auf dem Spalten Container, damit das Kanban Board beim Ziehen sichtbar reagiert. Diese Technik ist deutlich robuster als das direkte Manipulieren von Klassenlisten über classList, weil Alpine.js die Klassen konsistent mit dem reaktiven State hält.
6. Sortierung innerhalb einer Spalte
Ein produktives Kanban Board braucht mehr als das Verschieben zwischen Spalten, es muss auch die Reihenfolge innerhalb einer Spalte per Drag and Drop ändern können. Dafür wird jede Karte selbst zur Drop Zone: beim dragover über einer anderen Karte berechnet man anhand der Mausposition relativ zur Kartenmitte, ob die gezogene Karte vor oder nach der Zielkarte eingefügt werden soll.
Die Berechnung nutzt getBoundingClientRect() der Zielkarte und vergleicht event.clientY mit der vertikalen Mitte des Elements. Liegt die Maus in der oberen Hälfte der Karte, wird die gezogene Karte davor eingefügt, in der unteren Hälfte danach. Dieses Kanban Board Pattern entspricht exakt dem Verhalten, das Nutzer aus Trello und GitHub Projects kennen, und lässt sich vollständig ohne externe Sortier Bibliothek umsetzen.
function kanbanBoard() {
return {
columns: [ /* ... */ ],
onDropOnCard(event, targetColumnId, targetCardId) {
event.preventDefault();
event.stopPropagation(); // prevent the column's own drop handler
const cardId = Number(event.dataTransfer.getData('text/plain'));
if (cardId === targetCardId) return;
const rect = event.currentTarget.getBoundingClientRect();
const insertAfter = event.clientY > rect.top + rect.height / 2;
let movedCard = null;
this.columns.forEach((col) => {
const idx = col.cards.findIndex((c) => c.id === cardId);
if (idx !== -1) movedCard = col.cards.splice(idx, 1)[0];
});
if (!movedCard) return;
const targetColumn = this.columns.find((col) => col.id === targetColumnId);
const targetIndex = targetColumn.cards.findIndex((c) => c.id === targetCardId);
const insertIndex = insertAfter ? targetIndex + 1 : targetIndex;
targetColumn.cards.splice(insertIndex, 0, movedCard);
},
};
}
7. Persistenz: Board Zustand im localStorage speichern
Ein Kanban Board, das nach jedem Neuladen der Seite wieder im Ausgangszustand startet, ist für den echten Einsatz unbrauchbar. Alpine.js bringt mit dem persist Plugin genau die Lösung für dieses Problem: Alpine.$persist(defaultValue).as('key') ersetzt eine gewöhnliche State Eigenschaft durch eine Variante, die bei jeder Änderung automatisch in localStorage geschrieben wird und beim nächsten Laden den zuletzt gespeicherten Wert wiederherstellt.
Beim Kanban Board reicht es, die komplette columns Eigenschaft über persist zu verwalten, weil das Array die gesamte Board Struktur enthält. Für Boards mit mehreren Nutzern oder Server Synchronisation ist localStorage naturgemäß nur eine clientseitige Zwischenlösung, für persönliche Boards, interne Tools oder Prototypen ist es jedoch oft ausreichend und spart eine komplette Backend Anbindung.
// Requires the Alpine persist plugin (Alpine.plugin(persist))
document.addEventListener('alpine:init', () => {
Alpine.data('kanbanBoard', () => ({
columns: Alpine.$persist([
{ id: 'todo', title: 'Offen', cards: [] },
{ id: 'doing', title: 'In Arbeit', cards: [] },
{ id: 'done', title: 'Fertig', cards: [] },
]).as('kanban-board-columns'),
moveCardToColumn(cardId, targetColumnId) {
// Same logic as before — persist plugin handles storage transparently
let movedCard = null;
this.columns.forEach((col) => {
const idx = col.cards.findIndex((c) => c.id === cardId);
if (idx !== -1) movedCard = col.cards.splice(idx, 1)[0];
});
if (!movedCard) return;
this.columns.find((c) => c.id === targetColumnId).cards.push(movedCard);
},
}));
});
8. Barrierefreiheit: Tastatur Alternative zum Drag and Drop
Reines Drag and Drop schließt Tastaturnutzer und Screenreader Anwender systematisch aus, ein Problem, das viele fertige Kanban Board Bibliotheken ebenfalls nicht lösen. Ein zugängliches Kanban Board braucht eine zweite, gleichwertige Bedienmöglichkeit: Buttons oder Tastenkürzel, mit denen eine fokussierte Karte in die nächste oder vorherige Spalte verschoben werden kann, ohne dass eine Maus im Spiel ist.
Praktisch lässt sich das mit zwei kleinen Pfeil Buttons pro Karte lösen, die per @click dieselbe moveCardToColumn Methode aufrufen, die auch der Drag Handler nutzt. Zusätzlich sollte jede Karte aria-roledescription="draggable item" und eine aria-label mit dem aktuellen Spaltennamen tragen, damit Screenreader Nutzer verstehen, wo sich eine Karte gerade befindet. Diese kleine Ergänzung macht aus einem visuell beeindruckenden Kanban Board ein tatsächlich für alle nutzbares Werkzeug.
function kanbanBoard() {
return {
columns: [ /* ... */ ],
columnIndex(columnId) {
return this.columns.findIndex((c) => c.id === columnId);
},
// Keyboard-accessible alternative to native drag and drop
moveCardToAdjacentColumn(cardId, currentColumnId, direction) {
const currentIndex = this.columnIndex(currentColumnId);
const targetIndex = currentIndex + direction; // -1 or +1
if (targetIndex < 0 || targetIndex >= this.columns.length) return;
this.moveCardToColumn(cardId, this.columns[targetIndex].id);
},
};
}
9. Kanban Board Ansätze im Vergleich
Bevor man sich für die native Drag and Drop API entscheidet, lohnt sich ein Blick auf die Alternativen, die für ein Kanban Board in Frage kommen. Jede Variante hat spezifische Vor- und Nachteile bei Bundle Größe, Touch Support und Implementierungsaufwand.
| Ansatz | Bundle Größe | Touch Support | Eignung |
|---|---|---|---|
| Native Drag and Drop plus Alpine.js | 0 KB zusätzlich | Nur mit Zusatzcode | Volle Kontrolle, kleine Boards |
| SortableJS | circa 13 KB gzip | Sehr gut | Große Boards, komplexe Regeln |
| jQuery UI Sortable | circa 30 KB gzip mit jQuery | Schwach | Nur in Legacy Projekten |
| Framework Bibliothek (z.B. react-beautiful-dnd) | circa 30 bis 40 KB gzip | Sehr gut | Nur in React/Vue Apps sinnvoll |
Für die meisten internen Tools, Admin Bereiche und mittelgroße Boards überwiegt bei einem Kanban Board mit Alpine.js der Vorteil der Nullkosten Bundle Größe. Sobald jedoch native Touch Gesten auf Mobilgeräten Pflicht sind, lohnt der zusätzliche Aufwand für Pointer Events oder eine schlanke Touch Polyfill Ergänzung, da die native HTML5 Drag and Drop API auf Touchscreens traditionell schwach unterstützt wird.
Mironsoft
Alpine.js Komponenten für Hyvä, Magento und individuelle Frontends
Ein eigenes Kanban Board oder eine andere Alpine.js Komponente nötig?
Wir entwickeln maßgeschneiderte Alpine.js Komponenten, von Drag and Drop Boards über Datenlisten bis zu komplexen Formularen, sauber integriert in euer bestehendes Hyvä oder Magento Frontend.
Konzeption
Datenmodell und Interaktionsmuster für euer Board oder eure Komponente
Umsetzung
Native Drag and Drop, Persistenz und Barrierefreiheit aus einer Hand
Integration
Saubere Einbindung in bestehende Hyvä und Magento Frontends
10. Zusammenfassung
Ein vollständiges Kanban Board lässt sich mit Alpine.js und der nativen HTML5 Drag and Drop API bauen, ohne eine einzige zusätzliche Bibliothek zu laden. Die Datenstruktur besteht aus einem Array von Spalten mit verschachtelten Karten Arrays, die Interaktion läuft über die Events dragstart, dragover und drop, und Alpine.js sorgt mit seinem reaktiven Proxy System dafür, dass jede Änderung an den Daten sofort in der Ansicht sichtbar wird.
Für ein produktionsreifes Kanban Board kommen drei Ergänzungen dazu: visuelles Feedback über eine Drop Zonen Statusvariable, Sortierung innerhalb einer Spalte über Positionsberechnung mit getBoundingClientRect(), und eine Tastatur Alternative für Nutzer, die kein Drag and Drop ausführen können oder wollen. Mit dem persist Plugin wird der komplette Board Zustand zusätzlich dauerhaft im Browser gespeichert, ganz ohne Backend Anbindung.
Kanban Board mit Alpine.js — Das Wichtigste auf einen Blick
Datenstruktur
Array aus Spalten Objekten, jede Spalte mit eigenem cards Array. Flach und serialisierbar für Persistenz.
Drag Events
dragstart speichert die Karten ID, dragover muss preventDefault() aufrufen, drop verschiebt die Karte.
Persistenz
Alpine persist Plugin speichert den kompletten Board Zustand automatisch im localStorage.
Barrierefreiheit
Pfeil Buttons als Tastatur Alternative zum Drag and Drop, aria-label mit aktueller Spalte pro Karte.