Skip to content

API Overview

SymbolPurposeExecution modeCommon gotcha
createDropZone()Create a typed drop-zone controllerSyncDispose the controller during teardown
createSortable()Add sortable drag-and-drop behavior to listsSyncProvide stable item identity for reorder operations
createSortableScope()Create a shared scope for connected listsSyncEach set of connected containers needs its own scope instance
applyReorder()Apply ordered IDs to data arraysSyncUnknown IDs are skipped; non-mentioned items are appended
DropZoneOptions.acceptFilter file types before processingSyncMismatch between MIME and extension can reject files unexpectedly
DropZoneOptions.maxFilesCap accepted files per dropSyncExcess accepted files become rejected; onDropRejected is called
matchesAccept()Test a single File against an accept listSyncExtension patterns are case-insensitive; empty list accepts all
DndErrorBase class for Dnd errorsSyncUse DndError.is() to narrow unknown errors

Package Entry Point

ImportPurpose
@vielzeug/dndMain exports and types

Types

Disposable

ts
interface Disposable {
  readonly disposed: boolean;
  readonly disposalSignal: AbortSignal;
  dispose(): void;
  [Symbol.dispose](): void;
}

DropZoneOptions

ts
interface DropZoneOptions {
  element: HTMLElement;
  accept?: string[];
  maxFiles?: number;
  onValidate?: (files: File[], context: DropValidationContext) => 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

ts
interface DropZone extends Disposable {
  readonly hovered: boolean;
  readonly validating: boolean;
}

DropValidationContext

ts
interface DropValidationContext {
  readonly signal: AbortSignal;
}

SortableOptions

ts
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

ts
interface AutoScrollOptions {
  edgeThreshold?: number;
  speed?: number;
  container?: boolean;
  viewport?: boolean;
}

ReorderEvent

ts
interface ReorderEvent {
  ids: string[];
  setRevert(fn: () => void): void;
}

Sortable

ts
interface Sortable extends Disposable {
  readonly isDragging: boolean;
  revert(): void;
  sync(): void;
}

SortableScope

ts
interface SortableScope extends Disposable {
  readonly isDragging: boolean;
  revert(): void;
}

SortableScopeOptions

ts
interface SortableScopeOptions {
  onMove?: (event: SortableMoveEvent) => void;
  touch?: boolean | SortableTouchOptions;
}

SortableMoveEvent

ts
interface SortableMoveEvent {
  readonly itemId: string;
  readonly source: HTMLElement;
  readonly sourceIds: string[];
  readonly target: HTMLElement;
  readonly targetIds: string[];
  setRevert(fn: () => void): void;
}

SortableTouchOptions

ts
interface SortableTouchOptions {
  preview?: false | ((item: HTMLElement) => HTMLElement | null);
}

preview returns a template that Dnd clones before mounting it as a transient touch preview, so returning an element from the sortable item does not reparent or remove caller-owned DOM. Return false to disable the preview.

createDropZone()

ts
declare function createDropZone(options: DropZoneOptions): DropZone;

Attaches drag-and-drop file handling to a DOM element. Returns a DropZone handle.

OptionTypeDefaultDescription
elementHTMLElementRequired. The element to attach drag listeners to.
acceptstring[][]Accepted file types. Empty array accepts everything. Each entry is a MIME type ('image/png'), MIME wildcard ('image/*'), or file extension ('.pdf').
maxFilesnumberMaximum files accepted per drop. Files beyond this limit are passed to onDropRejected. When omitted there is no limit.
onValidate(files, { signal }) => boolean | Promise<boolean>Optional async gating step. Return or resolve false to reject all accepted files. validating remains true until every operation settles; signal aborts on disposal.
disabledbooleanWhen 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[]) => voidCalled 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[]) => voidCalled with files that did not match accept, exceeded maxFiles, or were rejected by onValidate.
onHoverChange(hovered: boolean) => voidCalled when hover state toggles. Use this callback for drag-over styling.
onValidatingChange(validating: boolean) => voidCalled whenever the aggregate async validation state changes.
pastebooleanfalseWhen true, attaches a paste listener to window. Pasted files run through the same accept, maxFiles, and onValidate pipeline as dropped files.
onPaste(files: File[]) => voidCalled 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 (DataTransferItem has no filename); exact filtering is applied at drop time.
  • Hover state (hovered) only becomes true when the dragged payload passes the accept filter. Drags carrying rejected file types enter and leave the zone without triggering onHoverChange.
  • Hover state is reset on element drop and also global window drop/dragend to avoid stuck hover state when drags leave the viewport.
ts
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.

ts
console.log(zone.validating); // true between drop and onValidate resolution

zone.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.

ts
zone.dispose();

zone[Symbol.dispose]()

[Symbol.dispose](): void

Alias for dispose(). Called automatically when used with the using keyword.

ts
{
  using zone = createDropZone({ element: dropEl, onDrop: handleFiles });
} // zone.dispose() runs here

createSortable()

ts
declare function createSortable(options: SortableOptions): Sortable;

Makes the direct children of a container element reorderable via drag. Returns a Sortable handle.

createSortable adds drag and keyboard defaults only when callers have not already supplied semantics. Every changed attribute and inline style is restored to its prior value on disposal.

  • 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 which getKey returns 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, default true. Enables keyboard reordering with arrow keys plus Home and End.
  • axis: 'vertical' | 'horizontal', default 'vertical'. Controls midpoint calculation for placeholder insertion.
  • autoScroll: boolean | AutoScrollOptions, default true. Scrolls the container near its edges; enable viewport scrolling with autoScroll.viewport.
  • dragImage: HTMLElement | ((id, item, event) => HTMLElement | null | undefined). Custom native drag preview passed to dataTransfer.setDragImage(). A null or undefined return skips setDragImage entirely.
  • dragImageOffset: [number, number], default [0, 0]. The [x, y] hotspot offset passed to setDragImage. 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 captureLayout() setup.
  • onReorder: (event: ReorderEvent) => void. Called after a successful reorder (drag or keyboard), only when the order changed. Use event.setRevert(fn) to register a revert function that sortable.revert() will invoke.

Returns: Sortable

ts
const boardScope = createSortableScope({
  onMove: ({ itemId, sourceIds, targetIds }) => saveMove(itemId, sourceIds, targetIds),
  touch: true,
});

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()

ts
declare function createSortableScope(options?: SortableScopeOptions): SortableScope;

Use one scope per connected set of containers. onMove fires once for cross-list moves with both final orders; local reorders continue to call the sortable's onReorder.

ParameterTypeDescription
optionsSortableScopeOptionsOptional cross-list move callback and scope-owned touch configuration

Returns: SortableScope.

ts
import { createSortableScope } from '@vielzeug/dnd';

const scope = createSortableScope({
  onMove: ({ itemId, sourceIds, targetIds }) => {
    persistMove(itemId, sourceIds, targetIds);
  },
  touch: true,
});

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.

ts
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().

SortableScope Interface

scope.isDragging

readonly isDragging: boolean

true while any sortable registered to the scope is dragging.

scope.revert()

revert(): void

Calls and clears the rollback registered with SortableMoveEvent.setRevert() for the latest cross-list move. It is a no-op when no rollback is registered.

scope.dispose()

dispose(): void

Disposes scope-owned touch input and prevents registered lists from participating in future connected moves.

DOM Attributes

Dnd reads and writes the following DOM attributes:

  • data-dnd-item: internal marker applied by createSortable to children that return a truthy key from getKey. Restored on dispose().
  • draggable, roles, tabindex, and touchAction: managed only as needed and restored to their exact prior values on dispose().
  • data-dragging: set during drag, removed on dragend or dispose(). Use it as your styling hook for drag state.
  • data-dnd-handle: internal marker set by createSortable and sortable.sync(), removed by dispose(). 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-placeholder element.
  • style.touchAction = 'none' (inline style): set by createSortable and sortable.sync() on the item (or the handle, when handle is set), then restored on dispose().

CSS Classes

ClassApplied toWhen
dnd-placeholder<div> inserted by sortableWhile an item is being dragged, in the placeholder's position

matchesAccept()

ts
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.

Returns: true when the file matches at least one pattern, or when accept is empty.

ts
import { matchesAccept } from '@vielzeug/dnd';

matchesAccept(file, ['image/*', '.pdf']); // true or false

applyReorder()

ts
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 items are ignored.
  • Items not listed in ids are appended in original order.
  • Duplicate IDs in ids — first occurrence wins, later occurrences are ignored.

Returns: A new array ordered by ids, with omitted items appended in their original order.

ts
const next = applyReorder(items, orderedIds, (item) => item.id);

Errors

ErrorTriggerNotable property
DndErrorBase class for package errorsDndError.is(error)
DndScopeErrorA sortable receives a scope not created by createSortableScope()