API Overview
| Symbol | Purpose | Execution mode | Common gotcha |
|---|---|---|---|
createDropZone() | Create a typed drop-zone controller | Sync | Remember to destroy the controller during teardown |
createSortable() | Add sortable drag-and-drop behavior to lists | Sync | Provide stable item identity for reorder operations |
createSortableScope() | Create a shared scope for connected lists | Sync | Each set of connected containers needs its own scope instance |
applyReorder() | Apply ordered IDs to data arrays | Sync | Unknown IDs are skipped; non-mentioned items are appended |
DropZoneOptions.accept | Filter file types before processing | Sync | Mismatch between MIME and extension can reject files unexpectedly |
DropZoneOptions.maxFiles | Cap accepted files per drop | Sync | Excess accepted files become rejected; onDropRejected is called |
matchesAccept() | Test a single File against an accept list | Sync | Extension patterns are case-insensitive; empty list accepts all |
Package Entry Point
| Import | Purpose |
|---|---|
@vielzeug/dnd | Main exports and types |
Types
Disposable
interface Disposable {
readonly disposed: boolean;
readonly disposalSignal: AbortSignal;
dispose(): void;
[Symbol.dispose](): void;
}DropZoneOptions
interface DropZoneOptions {
element: HTMLElement;
accept?: string[];
maxFiles?: number;
onValidate?: (files: File[]) => boolean | Promise<boolean>;
disabled?: boolean;
dropEffect?: DataTransfer['dropEffect'];
onDrop?: (files: File[]) => void;
onDropRejected?: (files: File[]) => void;
onHoverChange?: (hovered: boolean) => void;
onValidatingChange?: (validating: boolean) => void;
paste?: boolean;
onPaste?: (files: File[]) => void;
}DropZone
interface DropZone extends Disposable {
readonly hovered: boolean;
readonly validating: boolean;
}SortableOptions
interface SortableOptions {
element: HTMLElement;
getKey: (element: HTMLElement) => string;
scope?: SortableScope;
handle?: string;
keyboard?: boolean;
axis?: 'vertical' | 'horizontal';
autoScroll?: boolean | AutoScrollOptions;
dragImage?: HTMLElement | ((id: string, item: HTMLElement, event: DragEvent) => HTMLElement | null | undefined);
dragImageOffset?: [number, number];
placeholderClass?: string;
disabled?: boolean;
onDragStart?: (id: string, event: DragEvent) => void;
onDragEnd?: (id: string, event: DragEvent) => void;
onBeforeReorder?: (from: string[], to: string[]) => void;
onReorder?: (event: ReorderEvent) => void;
}AutoScrollOptions
interface AutoScrollOptions {
edgeThreshold?: number;
speed?: number;
container?: boolean;
viewport?: boolean;
}Sortable
interface Sortable extends Disposable {
readonly isDragging: boolean;
revert(): void;
sync(): void;
}SortableScope
declare function createSortableScope(): SortableScope;Creates an explicit connection scope for multi-container sorting. Containers only exchange items when they share the same scope instance.
createDropZone()
declare function createDropZone(options: DropZoneOptions): DropZone;Attaches drag-and-drop file handling to a DOM element. Returns a DropZone handle.
| Option | Type | Default | Description |
|---|---|---|---|
element | HTMLElement | — | Required. The element to attach drag listeners to. |
accept | string[] | [] | Accepted file types. Empty array accepts everything. Each entry is a MIME type ('image/png'), MIME wildcard ('image/*'), or file extension ('.pdf'). |
maxFiles | number | — | Maximum files accepted per drop. Files beyond this limit are passed to onDropRejected. When omitted there is no limit. |
onValidate | (files: File[]) => boolean | Promise<boolean> | — | Optional async gating step. Called after type/accept/maxFiles filtering, before onDrop. Return or resolve false to reject all accepted files. zone.validating is true while a promise is pending. Only receives type-accepted files. |
disabled | boolean | — | When true, all drag and paste events are ignored. A disabled zone does not call preventDefault on dragenter, dragover, drop, or paste, so underlying elements (text editors, etc.) receive them normally. |
dropEffect | 'copy' | 'move' | 'link' | 'none' | 'copy' | The dropEffect set on dataTransfer during dragover. Controls the cursor indicator. |
onDrop | (files: File[]) => void | — | Called with accepted files only. Not called if all dropped files are rejected. Also receives paste events when paste: true and onPaste is omitted. |
onDropRejected | (files: File[]) => void | — | Called with files that did not match accept, exceeded maxFiles, or were rejected by onValidate. |
onHoverChange | (hovered: boolean) => void | — | Called when hover state toggles. Use this callback for drag-over styling. |
paste | boolean | false | When true, attaches a paste listener to window. Pasted files run through the same accept, maxFiles, and onValidate pipeline as dropped files. |
onPaste | (files: File[]) => void | — | Called when files are pasted from the clipboard. Falls back to onDrop when omitted. Only active when paste: true. |
Returns: DropZone
Notes:
- Extension accept patterns are approximate during pre-check (
DataTransferItemhas no filename); exact filtering is applied at drop time. - Hover state (
hovered) only becomestruewhen the dragged payload passes theacceptfilter. Drags carrying rejected file types enter and leave the zone without triggeringonHoverChange. - Hover state is reset on element drop and also global
windowdrop/dragendto avoid stuck hover state when drags leave the viewport.
const zone = createDropZone({
element: dropEl,
accept: ['image/*', '.pdf'],
onDrop: (files) => {
upload(files);
},
onDropRejected: (files) => {
showError(`${files.length} rejected`);
},
onHoverChange: (hovered) => {
dropEl.classList.toggle('drag-over', hovered);
},
});DropZone Interface
zone.hovered
readonly hovered: boolean
true when a drag is currently over the zone. Updated synchronously by the internal counter — safe to read at any time.
zone.validating
readonly validating: boolean
true while an onValidate promise is pending. Use this to render a loading indicator between file selection and the acceptance/rejection callbacks firing.
console.log(zone.validating); // true between drop and onValidate resolutionzone.disposed
readonly disposed: boolean
true once dispose() has been called. Safe to read at any time.
zone.disposalSignal
readonly disposalSignal: AbortSignal
An AbortSignal that fires when dispose() is called. Use it to cancel in-flight requests tied to the zone's lifetime.
zone.dispose()
dispose(): void
Removes all event listeners from the element, resets the drag counter and hover state, and clears the hovered flag. Idempotent — safe to call multiple times.
zone.dispose();zone[Symbol.dispose]()
[Symbol.dispose](): void
Alias for dispose(). Called automatically when used with the using keyword.
{
using zone = createDropZone({ element: dropEl, onDrop: handleFiles });
} // zone.dispose() runs herecreateSortable()
declare function createSortable(options: SortableOptions): Sortable;Makes the direct children of a container element reorderable via drag. Returns a Sortable handle.
createSortable sets draggable="true" and role="listitem" on qualifying children and sets role="list" on the container at initialization. After DOM mutations, call sortable.sync() to re-apply sortable attributes explicitly.
element:HTMLElement, required. The container whose children become sortable.getKey:(element: HTMLElement) => string, required. Maps each item element to its stable string identity. Children for whichgetKeyreturns a falsy value are skipped.scope:SortableScope, default private scope. Connects sortable lists explicitly; containers only exchange items when they share the same scope instance.handle:string. CSS selector for a drag handle inside each item. When omitted, the whole item is draggable.keyboard:boolean, defaulttrue. Enables keyboard reordering with arrow keys plusHomeandEnd.axis:'vertical' | 'horizontal', default'vertical'. Controls midpoint calculation for placeholder insertion.autoScroll:boolean | AutoScrollOptions, defaulttrue. Scrolls the container near its edges; enable viewport scrolling withautoScroll.viewport.dragImage:HTMLElement | ((id, item, event) => HTMLElement | null | undefined). Custom native drag preview passed todataTransfer.setDragImage(). Anullorundefinedreturn skipssetDragImageentirely.dragImageOffset:[number, number], default[0, 0]. The[x, y]hotspot offset passed tosetDragImage. Controls which point of the preview image follows the cursor.placeholderClass:string, default'dnd-placeholder'. CSS class applied to the generated placeholder element.disabled:boolean. Blocks drag interactions. If a list becomes disabled mid-drag, Dnd cancels the drag and restores the original order.onDragStart:(id: string, event: DragEvent) => void. Called when a drag starts.onDragEnd:(id: string, event: DragEvent) => void. Called when a drag ends, whether completed or cancelled.onBeforeReorder:(from: string[], to: string[]) => void. Called with the before/after order snapshots just before a successful reorder commits — for both drag and keyboard. Items are still in their pre-commit positions at the time of the call, making it ideal for FLIP animation setup.onReorder:(event: ReorderEvent) => void. Called after a successful reorder (drag or keyboard), only when the order changed. Useevent.setRevert(fn)to register a revert function thatsortable.revert()will invoke.
Returns: Sortable
const boardScope = createSortableScope();
const sortable = createSortable({
element: listEl,
getKey: (el) => el.dataset.id!,
handle: '.drag-handle',
onDragStart: (id) => {
listEl.classList.add('sorting');
},
onDragEnd: (id) => {
listEl.classList.remove('sorting');
},
onReorder: ({ ids, setRevert }) => {
const prev = currentOrder;
saveOrder(ids);
setRevert(() => saveOrder(prev));
},
scope: boardScope,
});createSortableScope()
declare function createSortableScope(): SortableScope;Use one scope per connected set of containers. Sortables without an explicit scope use a private scope and remain isolated.
Sortable Interface
sortable.isDragging
readonly isDragging: boolean
true while an item drag is in progress.
sortable.revert()
revert(): void
Calls the revert function registered via setRevert in the last onReorder invocation (if any) and clears it. A no-op when no revert function was registered or it has already been consumed. Works for both drag-based and keyboard-based reorders.
Only the most recent reorder can be reverted — a new reorder overwrites the stored function.
const sortable = createSortable({
element: listEl,
getKey: (el) => el.dataset.sortId!,
onReorder: ({ ids, setRevert }) => {
const prev = currentOrder;
setOrder(ids);
setRevert(() => setOrder(prev)); // ← enable revert
},
});
// On server error:
try {
await api.saveOrder(ids);
} catch {
sortable.revert();
}sortable.sync()
sync(): void
Re-applies draggable, role, and handle attributes after DOM mutations. Call it after adding, removing, or replacing sortable children.
sortable.disposed
readonly disposed: boolean
true once dispose() has been called.
sortable.disposalSignal
readonly disposalSignal: AbortSignal
An AbortSignal that fires when dispose() is called.
sortable.dispose()
dispose(): void
Removes all event listeners from the container, strips sortable attributes from items and handles, and cancels any in-progress drag by restoring the original order. Idempotent — safe to call multiple times.
sortable[Symbol.dispose]()
[Symbol.dispose](): void
Alias for dispose().
DOM Attributes
Dnd reads and writes the following DOM attributes:
data-dnd-item: internal marker applied bycreateSortableto children that return a truthy key fromgetKey. Removed bydispose().draggable: set bycreateSortableandsortable.sync(), removed bydispose(). Enables native drag on each item or handle.role="list": set bycreateSortable, removed bydispose(). Accessibility role on the container.role="listitem": set bycreateSortableandsortable.sync(), removed bydispose(). Accessibility role on each item.data-dragging: set during drag, removed ondragendordispose(). Use it as your styling hook for drag state.data-dnd-handle: internal marker set bycreateSortableandsortable.sync(), removed bydispose(). Lets Dnd clean up only the handle attributes it applied.aria-hidden="true": set on placeholder creation and removed with the placeholder. Applied to the.dnd-placeholderelement.
CSS Classes
| Class | Applied to | When |
|---|---|---|
dnd-placeholder | <div> inserted by sortable | While an item is being dragged, in the placeholder's position |
matchesAccept()
declare function matchesAccept(file: File, accept: string[]): boolean;Tests whether a File matches an accept pattern list. Each pattern can be:
- A MIME type:
'image/png' - A MIME wildcard:
'image/*' - A file extension:
'.pdf'
An empty list accepts everything. Extension matching is case-insensitive.
import { matchesAccept } from '@vielzeug/dnd';
matchesAccept(file, ['image/*', '.pdf']); // true or falseapplyReorder()
declare function applyReorder<T>(items: T[], ids: string[], getKey: (item: T) => string): T[];Applies a DOM reorder result (orderedIds) to your backing array.
- IDs missing from
itemsare ignored. - Items not listed in
idsare appended in original order. - Duplicate IDs in
ids— first occurrence wins, later occurrences are ignored.
const next = applyReorder(items, orderedIds, (item) => item.id);