ohne dnd-kit – mit der nativen HTML5 API
Externe DnD-Bibliotheken lösen Probleme, die in vielen Projekten gar nicht existieren – und bringen dafür Bundle-Größe, Lernaufwand und Abhängigkeiten mit. Die native HTML5 Drag and Drop API reicht für sortierbare Listen, Datei-Upload und einfache Drag-Interaktionen vollkommen aus – mit einem Custom Hook in unter 100 Zeilen TypeScript.
Inhaltsverzeichnis
- 1. Warum überhaupt nativ statt einer Bibliothek?
- 2. Die HTML5 Drag and Drop API erklärt
- 3. DnD-Events in React: Besonderheiten und Fallstricke
- 4. Sortierbare Liste: Schritt für Schritt
- 5. useSortable: DnD-Logik in einen Custom Hook
- 6. Datei-Upload mit Drag and Drop
- 7. Visuelles Feedback: Ghost, Overlay und Drop-Indikator
- 8. Touch-Support und mobile Einschränkungen
- 9. Nativ vs. dnd-kit: Wann welche Wahl?
- 10. Zusammenfassung
- 11. FAQ
1. Warum überhaupt nativ statt einer Bibliothek?
Externe Drag-and-Drop-Bibliotheken wie dnd-kit oder react-dnd lösen echte Probleme: komplexe Drag-Kanäle zwischen verschiedenen Listen, Keyboard-Support für Barrierefreiheit, Touch-Unterstützung über die Pointer Events API, virtualisierte Listen mit Tausenden von Einträgen und komplexe Drag-Overlay-Logik. Für diese Anforderungen sind sie ausgezeichnete Werkzeuge. Für einen einfachen Task-Manager, eine sortierbare Bild-Galerie oder einen Datei-Upload mit Drag-Unterstützung sind sie Overengineering – mit mehreren Kilobytes zusätzlicher Bundle-Größe, einem nicht trivialen Lernaufwand und einer Abstraktionsschicht, die einfache Anpassungen erschwert.
Die HTML5 Drag and Drop API ist seit HTML5 in allen modernen Browsern verfügbar und vollständig ohne JavaScript-Bibliothek nutzbar. Für React-Anwendungen bedeutet das: ein Custom Hook in unter hundert Zeilen, keine externen Abhängigkeiten, keine Breaking Changes bei Updates der Bibliothek, direkte Kontrolle über jeden Aspekt des Drag-Verhaltens. Das ist nicht in jedem Projekt die richtige Wahl, aber in mehr Projekten als man denkt.
Die Entscheidung für native DnD sollte bewusst fallen. Die nativen Events haben bekannte Eigenheiten (kein Touch-Support, Reihenfolge der Events nicht intuitiv, dragenter und dragleave feuern bei Kind-Elementen). Diese Fallstricke sind dokumentiert und lösbar – aber man muss sie kennen. Dieser Artikel behandelt genau diese Eigenheiten und zeigt, wie man sie in React-Projekten sauber handhabt.
2. Die HTML5 Drag and Drop API erklärt
Die HTML5 Drag and Drop API besteht aus sieben Events, die auf zwei verschiedene Elemente verteilt sind: das Drag-Element (das gezogene Element) und das Drop-Zone-Element (das Ziel). Das Drag-Element feuert dragstart (Beginn des Drags), drag (während des Drags, sehr häufig) und dragend (Ende des Drags). Die Drop-Zone feuert dragenter (Drag betritt Zone), dragover (Drag bewegt sich über Zone), dragleave (Drag verlässt Zone) und drop (Loslassen über Zone).
Das DataTransfer-Objekt ist das Kommunikationsmedium zwischen Drag-Element und Drop-Zone. Im dragstart-Handler schreibt man Daten mit event.dataTransfer.setData('text/plain', data), im drop-Handler liest man sie mit event.dataTransfer.getData('text/plain'). Für React-Anwendungen ist es oft einfacher, die zu übertragenden Daten in einem Ref zu halten, statt sie über DataTransfer zu serialisieren und deserialisieren.
// sortable-list.tsx — Basic sortable list with native HTML5 DnD
import { useState, useRef, type DragEvent } from 'react';
interface Item { id: string; label: string; }
function SortableList({ initialItems }: { initialItems: Item[] }) {
const [items, setItems] = useState<Item[]>(initialItems);
// Store dragged item index in a ref — no re-render needed
const draggedIndex = useRef<number | null>(null);
const dragOverIndex = useRef<number | null>(null);
function handleDragStart(e: DragEvent<HTMLLIElement>, index: number) {
draggedIndex.current = index;
// Required for Firefox — must call setData, content doesn't matter
e.dataTransfer.setData('text/plain', String(index));
e.dataTransfer.effectAllowed = 'move';
}
function handleDragOver(e: DragEvent<HTMLLIElement>, index: number) {
e.preventDefault(); // Required to allow drop
e.dataTransfer.dropEffect = 'move';
dragOverIndex.current = index;
}
function handleDrop(e: DragEvent<HTMLLIElement>) {
e.preventDefault();
const from = draggedIndex.current;
const to = dragOverIndex.current;
if (from === null || to === null || from === to) return;
// Immutable array reorder
const next = [...items];
const [removed] = next.splice(from, 1);
next.splice(to, 0, removed);
setItems(next);
draggedIndex.current = null;
dragOverIndex.current = null;
}
function handleDragEnd() {
draggedIndex.current = null;
dragOverIndex.current = null;
}
return (
<ul className="space-y-2">
{items.map((item, index) => (
<li
key={item.id}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
className="flex items-center gap-3 px-4 py-3 bg-white border border-slate-200 rounded-xl cursor-grab active:cursor-grabbing select-none"
>
<span className="text-slate-400">⋮⋮</span>
{item.label}
</li>
))}
</ul>
);
}
3. DnD-Events in React: Besonderheiten und Fallstricke
React synthetisiert Drag Events wie alle anderen DOM Events. Der wichtigste Unterschied zu nativem JavaScript: Alle React-Event-Handler müssen als JSX-Props übergeben werden (onDragStart, onDragOver, etc.), nicht mit addEventListener. Das ist für die meisten Events keine Überraschung, hat aber bei Drag and Drop eine wichtige Konsequenz: React verwendet Event-Delegation auf dem Root-Element, was bedeutet, dass Events immer bubbling nutzen.
Das bekannteste Fallstrick der HTML5 DnD API betrifft dragenter und dragleave: Wenn ein Drag-Element über ein Kind-Element bewegt wird, feuert dragleave für das Elternelement und sofort danach dragenter für dasselbe Elternelement (wegen Bubbling). Das erzeugt ein visuelles Flackern der Drop-Zone-Hervorhebung. Die Lösung ist ein Counter-Ansatz: Bei dragenter inkrementiert man einen Counter, bei dragleave dekrementiert man ihn. Nur wenn der Counter Null erreicht, ist der Drag wirklich aus der Zone herausgegangen. Dieser Counter kann in einem Ref gehalten werden, um Re-renders zu vermeiden.
4. Sortierbare Liste: Schritt für Schritt
Eine vollständige sortierbare Liste braucht neben der Drag-Logik auch visuelles Feedback: Die gezogene Karte sollte halbtransparent werden, die potenzielle Drop-Position sollte mit einem Indikator markiert sein. Das zweite Detail erreicht man in der nativen DnD API am einfachsten über CSS-Klassen, die basierend auf dem dragOverIndex-State gesetzt werden. Ein data-drag-over-Attribut am jeweiligen Listen-Element und ein CSS-Selektor [data-drag-over] { border-top: 2px solid ... } reicht für einfaches visuelles Feedback.
Das Opacity-Feedback für das gezogene Element ist etwas trickreicher. Man könnte einen State nutzen (const [draggedId, setDraggedId] = useState(null)), aber das verursacht Re-renders aller Listen-Elemente bei jedem DragStart und DragEnd. Besser ist die Verwendung eines Refs in Kombination mit direkter DOM-Manipulation im Event-Handler: event.currentTarget.style.opacity = '0.5' im dragstart und Rücksetzen im dragend. Das ist direkter Eingriff in den DOM, aber vertretbar für kurzlebige visuelle Zustände während eines Drags.
// use-sortable.ts — Custom Hook encapsulating all DnD list logic
import { useState, useRef, useCallback, type DragEvent } from 'react';
interface SortableItem { id: string; [key: string]: unknown; }
interface UseSortableReturn<T> {
items: T[];
getDragProps: (index: number) => {
draggable: true;
onDragStart: (e: DragEvent<HTMLElement>) => void;
onDragOver: (e: DragEvent<HTMLElement>) => void;
onDrop: (e: DragEvent<HTMLElement>) => void;
onDragEnd: () => void;
'data-drag-index': number;
};
dragOverIndex: number | null;
}
export function useSortable<T extends SortableItem>(initialItems: T[]): UseSortableReturn<T> {
const [items, setItems] = useState<T[]>(initialItems);
const [dragOverIdx, setDragOverIdx] = useState<number | null>(null);
const draggedIndexRef = useRef<number | null>(null);
const getDragProps = useCallback((index: number) => ({
draggable: true as const,
'data-drag-index': index,
onDragStart: (e: DragEvent<HTMLElement>) => {
draggedIndexRef.current = index;
e.dataTransfer.setData('text/plain', String(index));
e.dataTransfer.effectAllowed = 'move';
// Slight delay so browser captures element before opacity change
requestAnimationFrame(() => {
(e.target as HTMLElement).style.opacity = '0.4';
});
},
onDragOver: (e: DragEvent<HTMLElement>) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (dragOverIdx !== index) setDragOverIdx(index);
},
onDrop: (e: DragEvent<HTMLElement>) => {
e.preventDefault();
const from = draggedIndexRef.current;
if (from === null || from === index) return;
setItems(prev => {
const next = [...prev];
const [moved] = next.splice(from, 1);
next.splice(index, 0, moved);
return next;
});
setDragOverIdx(null);
draggedIndexRef.current = null;
},
onDragEnd: () => {
setDragOverIdx(null);
draggedIndexRef.current = null;
// Reset opacity on all items
document.querySelectorAll('[data-drag-index]').forEach(el => {
(el as HTMLElement).style.opacity = '';
});
},
}), [index, dragOverIdx]);
return { items, getDragProps, dragOverIndex: dragOverIdx };
}
6. Datei-Upload mit Drag and Drop
Datei-Upload mit Drag and Drop ist ein häufiger Anwendungsfall, der sich komplett ohne externe Bibliotheken implementieren lässt. Die Drop-Zone ist ein div mit den Event-Handlern onDragOver, onDragEnter, onDragLeave und onDrop. Im drop-Handler sind die gedropten Dateien über event.dataTransfer.files als FileList verfügbar. Der wichtige Unterschied zu sortierbare Listen: Bei Datei-Drops muss man im dragover-Handler event.preventDefault() aufrufen, sonst öffnet der Browser die Datei statt sie zu droppen.
Die Validierung der gedroppten Dateien (Typ, Größe) erfolgt direkt im drop-Handler über die File-Eigenschaften file.type und file.size. Für eine bessere UX kann man bereits im dragenter-Handler prüfen, ob die Items des DataTransfer die akzeptierten MIME-Typen haben – das ist über event.dataTransfer.items möglich, ohne die Dateien selbst lesen zu müssen. Das erlaubt eine sofortige visuelle Rückmeldung (grüne Drop-Zone für akzeptierbare Dateien, rote für nicht akzeptierbare).
7. Visuelles Feedback: Ghost, Overlay und Drop-Indikator
Die native HTML5 DnD API generiert automatisch einen Screenshot des gezogenen Elements als Drag-Ghost. Diesen Ghost kann man mit event.dataTransfer.setDragImage(element, offsetX, offsetY) ersetzen. Ein typisches Muster: Man erstellt ein absolut positioniertes, visuell angepasstes Element außerhalb des Viewports (position: absolute; left: -9999px), übergibt es als Drag-Image und entfernt es nach dem Drag. In React passiert das komfortabel über einen Ref auf einen vorab gerenderten Ghost-Container.
Drop-Indikatoren zwischen Listenelementen (ein horizontaler Balken, der die Einfügeposition markiert) sind mit der nativen API deutlich aufwendiger als mit dnd-kit. Die einfachste Lösung: Berechne im dragover-Handler die Y-Position des Mauszeigers relativ zum Listenelement (event.clientY - rect.top) und entscheide, ob die Position in der oberen oder unteren Hälfte liegt. Setze dann einen data-drop-position="before"|"after"-Attribut, das über CSS den Balken anzeigt. Das ist etwas mehr Rechnung als mit einer Bibliothek, aber vollständig ohne externe Abhängigkeiten lösbar.
8. Touch-Support und mobile Einschränkungen
Die native HTML5 Drag and Drop API funktioniert auf Touch-Geräten nicht. Das ist der wichtigste Einschränkung und der Hauptgrund, warum Bibliotheken wie dnd-kit in vielen Projekten sinnvoll sind: Sie implementieren Touch-Support über die Pointer Events API oder Touch Events. Für Projekte, bei denen Drag and Drop nur auf Desktop relevant ist (Admin-Panels, Desktop-Anwendungen), ist das kein Problem. Für Consumer-Anwendungen mit mobilem Traffic ist es ein Ausschlusskriterium für natives DnD.
Eine pragmatische Lösung für Projekte mit gemischtem Traffic: Natives DnD für Desktop, alternative Interaktionen für Mobile. Statt Drag-to-Sort auf Mobile kann man Pfeil-Buttons für das Neuanordnen anbieten – einfacher zu implementieren, zugänglicher und für Touch-Interaktionen naturlicher. Diese progressive Enhancement-Strategie ist oft die bessere Nutzererfahrung als Touch-Drag-Simulationen, die sich auf mobilen Geräten unnatürlich anfühlen.
9. Nativ vs. dnd-kit: Wann welche Wahl?
Die Entscheidung zwischen nativer HTML5 DnD API und einer Bibliothek wie dnd-kit sollte von den konkreten Anforderungen des Projekts abhängen, nicht von der Wahl, immer die mächtigste oder immer die einfachste Lösung zu wählen.
| Kriterium | Nativ HTML5 DnD | dnd-kit |
|---|---|---|
| Touch-Support | Nicht vorhanden | Vollständig via Pointer Events |
| Bundle-Größe | 0 KB | ~28 KB gzip (mit Sortable) |
| Keyboard-Accessibility | Manuell implementieren | Eingebaut (ARIA, Keyboard) |
| Virtualisierte Listen | Komplex | Unterstützt via @dnd-kit/sortable |
| Drop-Indikatoren | Manuell implementieren | Automatisch via Overlay |
| Datei-Upload | Direkt via dataTransfer.files | Nicht primärer Anwendungsfall |
Die praktische Entscheidungsregel: Wenn die Anwendung mobil genutzt wird und DnD dort relevant ist, oder wenn Keyboard-Accessibility Anforderung ist, oder wenn virtualisierte Listen mit tausenden Einträgen sortierbar sein müssen – dann ist dnd-kit die richtige Wahl. Für Admin-Panels auf Desktop, einfache Datei-Upload-Zonen und sortierbare Listen mit wenigen Einträgen ohne Mobile-Anforderung ist die native HTML5 API völlig ausreichend und die einfachere Wahl.