Drag and Drop in Vue ohne UI-Chaos
AI generated
<v/>
{ }
Vue 3 · Drag and Drop · HTML5 API · vue-draggable · UX
Drag and Drop in Vue ohne UI-Chaos
von der HTML5-API bis zum Kanban-Board mit Touch-Support

Drag-and-Drop wirkt simpel, bis man Ghost-Images debuggt, Touch-Events auf Mobilgeräten nicht funktionieren und der Zustand nach dem Drop nicht konsistent ist. Vue 3 bietet mit der Composition API und spezialisierten Bibliotheken alle Bausteine für ein sauberes Drag-and-Drop ohne visuellen oder logischen Chaos.

15 Min. Lesezeit HTML5 Drag API · vue-draggable-plus · Touch-Events · Kanban · Accessibility Vue 3 · TypeScript · SortableJS

1. Warum Drag and Drop in Vue komplex ist

Drag and Drop in Vue beginnt mit der Frage: Welche Schicht übernimmt welche Verantwortung? Die HTML5 Drag API liefert die Browser-Events, aber das Zustandsmanagement — welches Element wird gezogen, auf welches Drop-Target zeigt der Cursor, in welcher Position soll es landen — ist vollständig Aufgabe des JavaScript-Codes. Vue's Reaktivitätssystem ist dafür ideal geeignet, weil Zustandsänderungen sofort in der UI sichtbar werden, ohne manuelles DOM-Manipulieren. Die Herausforderung liegt darin, das Drag-and-Drop-Zustandsmodell sauber zu definieren, bevor man mit der Implementierung beginnt.

Die häufigsten Probleme beim Drag and Drop in Vue entstehen nicht durch technische Schwierigkeiten, sondern durch konzeptionelle Unklarheiten: Ghost-Images, die den falschen Zustand abbilden, weil sie zu früh oder zu spät erstellt werden. Zustandsinkonsistenzen nach einem Drop, weil die Datenstruktur nicht atomisch aktualisiert wurde. Fehlende Touch-Event-Unterstützung, weil die HTML5 Drag API auf Mobilgeräten nicht oder nur eingeschränkt funktioniert. Und fehlende Keyboard-Navigation, die Drag and Drop für Tastaturnutzer und Screenreader-Nutzer unzugänglich macht. Dieser Artikel adressiert alle vier Problemkategorien.

2. Die HTML5 Drag and Drop API direkt in Vue nutzen

Die HTML5 Drag API funktioniert über ein System von Events: dragstart (Start des Drags), drag (während des Drags), dragend (Ende des Drags), und auf dem Drop-Target: dragenter, dragover, dragleave und drop. Damit ein Element draggable ist, braucht es das HTML-Attribut draggable="true". In Vue setzt man das als gebundenes Attribut: :draggable="true" oder auf allen Listenelementen über eine Komponente. Das dataTransfer-Objekt im dragstart-Event ist der Kommunikationskanal zwischen Drag-Quelle und Drop-Target: Man schreibt Daten mit setData() und liest sie im drop-Event mit getData() aus.

Ein wichtiger Aspekt der HTML5 Drag API beim Drag and Drop in Vue: Das dragover-Event muss mit event.preventDefault() behandelt werden, damit der Browser den Drop erlaubt. Ohne diese Zeile verweigert der Browser den Drop-Vorgang. Das dataTransfer.dropEffect und dataTransfer.effectAllowed steuern, welche Cursor-Symbole der Browser während des Drags anzeigt: copy, move, link oder none. Für die meisten Drag-and-Drop in Vue-Szenarien ist move die richtige Wahl, da Elemente von einer Position zur anderen verschoben werden.


<!-- SortableList.vue — basic drag-and-drop reordering with HTML5 API -->
<template>
  <ul class="space-y-2">
    <li
      v-for="(item, index) in items"
      :key="item.id"
      draggable="true"
      class="p-4 bg-white border border-slate-200 rounded-xl cursor-grab active:cursor-grabbing"
      :class="{ 'opacity-40 scale-95': dragState.draggingId === item.id }"
      @dragstart="onDragStart($event, item.id, index)"
      @dragend="onDragEnd"
      @dragover.prevent="onDragOver($event, index)"
      @drop.prevent="onDrop($event, index)"
      @dragleave="onDragLeave"
    >
      <slot :item="item" :isDragging="dragState.draggingId === item.id" />
    </li>
  </ul>
</template>

<script setup lang="ts">
import { reactive } from 'vue'

interface Item { id: string | number }

const props = defineProps<{ items: Item[] }>()
const emit = defineEmits<{ reorder: [from: number, to: number] }>()

const dragState = reactive({
  draggingId: null as string | number | null,
  fromIndex: -1,
  overIndex: -1,
})

function onDragStart(event: DragEvent, id: string | number, index: number) {
  dragState.draggingId = id
  dragState.fromIndex = index
  // Set drag data for cross-component communication
  event.dataTransfer!.setData('text/plain', String(id))
  event.dataTransfer!.effectAllowed = 'move'
}

function onDragOver(event: DragEvent, index: number) {
  event.dataTransfer!.dropEffect = 'move'
  dragState.overIndex = index
}

function onDrop(event: DragEvent, toIndex: number) {
  if (dragState.fromIndex !== -1 && dragState.fromIndex !== toIndex) {
    emit('reorder', dragState.fromIndex, toIndex)
  }
  resetDragState()
}

function onDragEnd() { resetDragState() }
function onDragLeave() { dragState.overIndex = -1 }

function resetDragState() {
  dragState.draggingId = null
  dragState.fromIndex = -1
  dragState.overIndex = -1
}
</script>

3. useDragAndDrop-Composable für einfache Szenarien

Für einfache Drag-and-Drop-Szenarien in Vue — eine einzelne sortierbare Liste, ein paar Drag-Targets — ist ein eigenes useDragAndDrop-Composable die wartbarste Lösung. Das Composable verwaltet den gesamten Drag-Zustand als reaktives Objekt und stellt Event-Handler als Funktionen nach außen bereit. Die Komponente importiert das Composable, bindet die Handler an die DOM-Elemente und enthält keine Drag-and-Drop-Logik mehr. Das Ergebnis ist eine Komponente, die ausschließlich Rendering-Logik enthält, und ein testbares Composable für das Drag-and-Drop-Verhalten.

Die Reorder-Funktion ist der Kern des Drag and Drop in Vue-Composables: Sie nimmt die Ursprungs- und Zielposition entgegen und aktualisiert das Items-Array atomar. Das klassische Muster: Element am Ursprungsindex entfernen und am Zielindex einfügen. Vue's Reaktivitätssystem sorgt dafür, dass das Template sofort neu rendert und die neue Reihenfolge anzeigt. Eine häufige Fehlerquelle: Die Indexe müssen nach dem Entfernen des Elements angepasst werden, weil das Entfernen eines Elements vor dem Zielindex den Zielindex um eins verschiebt. Das Composable kapselt diese Berechnung und verhindert Off-by-One-Bugs in der Komponente.

4. vue-draggable-plus für sortierbare Listen

vue-draggable-plus ist die empfohlene Bibliothek für Drag and Drop in Vue 3. Sie basiert auf SortableJS — der gleichen Bibliothek, die auch im verbreiteten vue-draggable für Vue 2 verwendet wird — und ist vollständig für Vue 3 mit TypeScript geschrieben. Das zentrale Pattern ist denkbar einfach: Man ersetzt das umhüllende Listen-Element durch eine <VueDraggable>-Komponente und übergibt das reaktive Array per v-model. Die Bibliothek übernimmt alle Drag-Events, die Ghost-Image-Erstellung und die Zustandsänderung im Array — ohne dass man einen einzigen Drag-Event-Handler schreiben muss.

Das v-model-Binding ist der Schlüssel zur sauberen Zustandsverwaltung: Die Bibliothek mutet das Array direkt nach einem erfolgreichen Drop. Da es sich um ein reaktives ref-Array handelt, aktualisiert Vue die UI automatisch. Für Szenarien mit serverseitiger Persistenz registriert man auf dem @update-Event der <VueDraggable>-Komponente und sendet die neue Reihenfolge an die API. Das trennt sauber zwischen der visuellen Reihenfolgeänderung (sofort, optimistisch) und der Persistenz (asynchron, mit Fehlerbehandlung). Wenn die API-Anfrage fehlschlägt, kann das Array auf den vorherigen Zustand zurückgesetzt werden.

5. Kanban-Board: Listen-übergreifendes Drag and Drop

Ein Kanban-Board ist der anspruchsvollste Standard-Anwendungsfall für Drag and Drop in Vue: Karten werden nicht nur innerhalb einer Spalte umsortiert, sondern auch zwischen Spalten verschoben. Das erfordert ein Zustandsmodell, das mehrere Listen verwaltet und Cross-List-Moves atomar abbildet. Mit vue-draggable-plus ist das über die group-Option gelöst: Alle Listen, die zum selben Drag-and-Drop-Kontext gehören, erhalten denselben Gruppen-Namen. Die Bibliothek erlaubt dann automatisch das Ziehen von Elementen zwischen diesen Listen.

Das Zustandsmodell eines Kanban-Boards in Vue folgt typischerweise diesem Muster: Ein reaktives Objekt oder ein Pinia-Store hält ein Dictionary von Spalten-ID zu Karten-Array. Wenn eine Karte von Spalte A nach Spalte B gezogen wird, wird das Karten-Objekt aus dem Array von Spalte A entfernt und in das Array von Spalte B eingefügt. Mit vue-draggable-plus passiert das automatisch über das v-model-Binding, aber der Code muss eine @add und @remove-Event-Behandlung implementieren, wenn die Persistenz die Spalten-ID als Teil der Karte speichert. Drag and Drop in Vue für Kanban-Boards lohnt sich durch die Entkopplung von visueller Interaktion und Zustandsmutation erheblich wartbarer zu sein.


<!-- KanbanBoard.vue — cross-list drag and drop with vue-draggable-plus -->
<template>
  <div class="flex gap-4 overflow-x-auto pb-4">
    <div
      v-for="column in columns"
      :key="column.id"
      class="flex-shrink-0 w-72 bg-slate-100 rounded-2xl p-4"
    >
      <h3 class="font-bold text-slate-800 mb-3">{ { column.title } }</h3>

      <VueDraggable
        v-model="column.cards"
        :group="{ name: 'kanban', pull: true, put: true }"
        item-key="id"
        class="min-h-16 space-y-2"
        ghost-class="opacity-30"
        chosen-class="shadow-xl scale-105"
        drag-class="rotate-2"
        animation="200"
        @add="(event) => onCardMoved(event, column.id)"
      >
        <template #item="{ element: card }">
          <div
            class="bg-white rounded-xl p-3 shadow-sm cursor-grab active:cursor-grabbing border border-slate-200"
          >
            <p class="font-semibold text-sm text-slate-800">{ { card.title } }</p>
            <p class="text-xs text-slate-500 mt-1">{ { card.assignee } }</p>
          </div>
        </template>
      </VueDraggable>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VueDraggable } from 'vue-draggable-plus'
import type { SortableEvent } from 'sortablejs'

interface Card { id: string; title: string; assignee: string; columnId: string }
interface Column { id: string; title: string; cards: Card[] }

const columns = ref<Column[]>([
  { id: 'todo', title: 'Offen', cards: [
    { id: '1', title: 'Login-Seite', assignee: 'Max', columnId: 'todo' },
    { id: '2', title: 'API-Integration', assignee: 'Lisa', columnId: 'todo' },
  ]},
  { id: 'in-progress', title: 'In Arbeit', cards: [] },
  { id: 'done', title: 'Fertig', cards: [] },
])

async function onCardMoved(event: SortableEvent, targetColumnId: string) {
  const cardId = event.item.dataset.id
  // Persist column change to API — optimistic update already applied by vue-draggable
  try {
    await updateCardColumn(cardId!, targetColumnId)
  } catch {
    // On API failure: revert the optimistic update
    // (re-fetch or restore previous state from snapshot)
  }
}

async function updateCardColumn(cardId: string, columnId: string) {
  await fetch(`/api/cards/${cardId}`, {
    method: 'PATCH',
    body: JSON.stringify({ columnId }),
    headers: { 'Content-Type': 'application/json' },
  })
}
</script>

6. Custom Ghost-Image und Drag-Overlay

Der Standard-Ghost, den der Browser beim Drag and Drop in Vue zeigt, ist ein pixeliertes Abbild des gezogenen Elements — oft unscharf und visuell unattraktiv. Mit dataTransfer.setDragImage() lässt sich dieses Ghost-Image durch ein beliebiges DOM-Element ersetzen. Das Muster: Ein benutzerdefiniertes Element wird kurz vor dem Drag-Start zum DOM hinzugefügt, als Ghost-Image registriert und unmittelbar danach wieder entfernt. Das Drag-API arbeitet dann mit der registrierten Referenz, auch wenn das Element aus dem DOM entfernt wurde.

Eine elegantere Alternative für Drag and Drop in Vue ist ein vollständig separates Overlay-Element, das die Position des Mauszeigers verfolgt. Dieses Overlay ist kein Browser-Ghost, sondern eine reguläre Vue-Komponente, die mit absolutem Positioning am Mauszeiger klebt. Die Mausposition wird über mousemove (oder pointermove) verfolgt und das Overlay per CSS-Transform verschoben. Dieser Ansatz gibt vollständige Kontrolle über das visuelle Erscheinungsbild des Drag-Elements, erfordert aber einen eigenen DnD-State-Manager, weil man die HTML5 Drag API nicht mehr für das visuelle Feedback nutzt.

7. Touch-Support und Mobile-UX

Die HTML5 Drag API funktioniert auf mobilen Geräten mit iOS und Android nur eingeschränkt oder gar nicht. Touch-Geräte kennen keine Drag-Events, sondern Touch-Events: touchstart, touchmove und touchend. Wer Drag and Drop in Vue auch auf Mobilgeräten anbieten will, muss Touch-Events manuell in Drag-Verhalten übersetzen oder eine Bibliothek verwenden, die das bereits macht. SortableJS — und damit auch vue-draggable-plus — hat eingebaute Touch-Unterstützung: Es erkennt Touch-Events und emuliert das Drag-and-Drop-Verhalten ohne zusätzliche Konfiguration.

Ein wichtiger UX-Aspekt beim Drag and Drop in Vue auf mobilen Geräten ist die Unterscheidung zwischen einem Scroll-Swipe und einem Drag-Beginn. SortableJS löst das über eine konfigurierbare delay-Option: Der Drag beginnt erst nach einer kurzen Haltezeit von z.B. 150–200 Millisekunden. Unter dieser Zeitgrenze interpretiert das System die Touch-Geste als Scroll. Das sollte in der UI kommuniziert werden: Ein visuelles Feedback (leichtes Vibrieren über die Vibration-API, eine Farb- oder Größenänderung des Elements) signalisiert dem Nutzer, dass der Drag begonnen hat. Ohne dieses Feedback fühlt sich Mobile-Drag-and-Drop unkontrolliert und unzuverlässig an.

8. Accessibility: Tastatur-Alternativen zu Drag and Drop

Drag and Drop in Vue ist für Nutzer mit motorischen Einschränkungen, Tastaturnutzer und Screenreader-Nutzer ohne Alternative unzugänglich. Die WCAG-Richtlinien (Success Criterion 2.1.1) fordern, dass alle Funktionalitäten, die per Maus erreichbar sind, auch per Tastatur bedienbar sein müssen. Für Sortieraufgaben bedeutet das konkret: Jedes sortierbare Element erhält eine Taste oder eine Tastenkombination, um in den "Ausgewählt für Verschiebung"-Modus zu wechseln, und Pfeiltasten bewegen das Element in der Liste. Ein aria-grabbed-Attribut (deprecated in ARIA 1.1, aber weiterhin nützlich) und entsprechende aria-live-Regionen geben Screenreader-Nutzern Feedback über den aktuellen Zustand.

Die praktische Implementierung der Tastatur-Alternative für Drag and Drop in Vue ist ein separates System neben dem Maus-Drag: Bei Leertaste oder Enter auf einem sortierbaren Element wird dieses als "aufgegriffen" markiert. Pfeiltasten verschieben es in der Liste. Erneutes Drücken von Leertaste oder Enter "legt es ab". Diese Interaktion ist vollständig über den reaktiven Zustand abbildbar — kein DOM-Drag-Event, nur Tastatur-Events und Array-Mutation. In vue-draggable-plus gibt es noch keine native Tastatur-Unterstützung, weshalb diese als separates useKeyboardSort-Composable implementiert werden sollte, das parallel zum Drag-and-Drop funktioniert.

9. Drag-and-Drop-Bibliotheken im Vergleich

Die Wahl der Bibliothek für Drag and Drop in Vue 3 hängt vom Anwendungsfall, der gewünschten Browser-Kompatibilität und der Touch-Unterstützung ab.

Bibliothek Vue 3 Touch-Support Stärken
HTML5 Drag API Nativ Eingeschränkt Keine Abhängigkeit, volle Kontrolle
vue-draggable-plus Ja (Vue 3) Ja Einfach, Gruppen, Animationen
dnd-kit (React) Nein Ja Nur für React relevant
Draggable (Shopify) Manuell integrierbar Ja Accessibility-Features eingebaut
VueDraggable (v2) Nur Vue 2 Ja Nicht für neue Vue-3-Projekte

Für neue Vue-3-Projekte ist vue-draggable-plus die klare Empfehlung: native Vue-3-Kompatibilität mit TypeScript, Touch-Support durch SortableJS, Cross-List-Drag via Groups und einfaches v-model-Binding. Für Projekte mit hohen Accessibility-Anforderungen empfiehlt sich die Kombination aus vue-draggable-plus für Maus/Touch und einem eigenem useKeyboardSort-Composable für Tastaturnutzer.

Mironsoft

Vue 3 Frontend-Entwicklung, Interaktions-Design und Accessibility-Engineering

Drag-and-Drop-Feature mit Zustandsproblemen oder fehlendem Touch-Support?

Wir implementieren vollständige Drag-and-Drop-Lösungen in Vue 3 – mit Touch-Support, Custom Ghost-Images, Cross-List-Drag, Keyboard-Alternativen und persistenter Zustandssynchronisation.

Kanban-Board

Vollständiges Kanban mit Cross-List-Drag, Persistenz und optimistischen Updates

Touch & Mobile

Drag-and-Drop auch auf iOS und Android mit visuell polierter UX

Accessibility

Tastatur-Alternativen und ARIA-Attribute für vollständige WCAG-Konformität

10. Zusammenfassung

Professionelles Drag and Drop in Vue 3 beginnt mit dem richtigen Zustandsmodell: Welches Element wird gezogen, wohin kann es fallen, und wie wird die Reorder-Operation atomar durchgeführt? Die HTML5 Drag API liefert die Browser-Events, Vue's Reaktivitätssystem übernimmt die Zustandsverwaltung. Für einfache Listen reicht ein eigenes Composable, für sortierbare Listen und Cross-List-Drag ist vue-draggable-plus die empfohlene Bibliothek mit einfachem v-model-Binding und eingebautem Touch-Support über SortableJS.

Die drei häufig vergessenen Aspekte beim Drag and Drop in Vue: Ghost-Images sollten benutzerdefiniert sein, statt auf den Browser-Default zu vertrauen. Touch-Support muss explizit getestet werden — iOS und Android haben abweichendes Verhalten bei der HTML5 Drag API. Und Keyboard-Alternativen sind keine optionale Erweiterung, sondern eine Accessibility-Anforderung. Ein useKeyboardSort-Composable, das parallel zum Drag-and-Drop läuft, macht das Feature für alle Nutzer zugänglich — ohne die Maus-UX zu kompromittieren.

Drag and Drop in Vue — Das Wichtigste auf einen Blick

Zustandsmodell

draggingId, fromIndex, overIndex als reaktives Objekt. Reorder-Funktion atomar ausführen. Niemals DOM direkt manipulieren — Vue übernimmt das Rendering.

vue-draggable-plus

v-model für automatische Array-Mutation. group-Option für Cross-List-Drag. animation-Option für Sortier-Animationen. Touch-Support inklusive.

Touch & Mobile

HTML5 Drag API auf Mobile nicht zuverlässig. SortableJS/vue-draggable-plus haben eingebauten Touch-Support. delay-Option verhindert Konflikt mit Scroll-Gesten.

Accessibility

WCAG 2.1.1: Tastatur-Alternative Pflicht. Leertaste/Enter zum Aufgreifen, Pfeiltasten zum Verschieben. aria-live-Region für Screenreader-Feedback.

11. FAQ: Drag and Drop in Vue ohne UI-Chaos

1HTML5 Drag API auf Mobile nicht funktioniert?
Mobile kennt nur Touch-Events. vue-draggable-plus oder SortableJS direkt verwenden – beide emulieren Drag über Touch-Events zuverlässig.
2vue-draggable vs. vue-draggable-plus?
vue-draggable = Vue 2, deprecated. vue-draggable-plus = Vue 3, TypeScript, v-model, aktiv entwickelt. Für neue Projekte immer vue-draggable-plus.
3Daten zwischen Drag und Drop übergeben?
dataTransfer.setData() / getData() für Browser-Kommunikation. Für Vue-interne Kommunikation: reaktives dragState-Objekt — sauberer und typsicher.
4Flackernde Drag-Overlays?
Counter-Ansatz: dragenter ++, dragleave --. isDragging = counter > 0. Kein Flackern bei Kindelementen.
5Reihenfolge persistieren?
Optimistisch per v-model aktualisieren, asynchron an API senden. Bei Fehler: Snapshot zurücksetzen und Fehlermeldung anzeigen.
6Cross-List-Drag mit vue-draggable-plus?
group-Option: Alle beteiligten Draggables denselben Gruppennamen geben. pull: true = Entnahme erlaubt, put: true = Ablage erlaubt.
7WCAG-Konformität für Drag and Drop?
Tastatur-Alternative Pflicht (WCAG 2.1.1). Leertaste zum Aufgreifen, Pfeiltasten zum Verschieben, aria-live für Screenreader.
8Custom Ghost-Image erstellen?
Element im dragstart erstellen, zum DOM hinzufügen, setDragImage() aufrufen, mit setTimeout sofort entfernen. Browser behält die Referenz.
9Warum :key kritisch bei sortierbaren Listen?
Ohne :key ordnet Vue Komponenten falsch zu. Bei Drag-and-Drop immer stabile ID des Elements als :key, nie den Array-Index.
10Drop-Zone nur während Drag anzeigen?
v-show='isDragging' auf der Drop-Zone. isDragging im globalen Drag-State (reactive oder Pinia): dragstart → true, dragend → false.