Skip to content
dnd logoDndUI Interaction
Framework-agnostic drag-and-drop. Drop zones with MIME filtering, sortable lists with drag handles, and explicit connected scopes — zero third-party dependencies.
Version
v3.0.0
Size
6.3 KB gzip
Browser
createDropZonecreateSortablecreateSortableScopeapplyReordermatchesAccept

Why Dnd?

The HTML5 Drag & Drop API requires careful counter tracking to avoid hover state flicker, has no MIME type pre-filtering, and provides no sortable list abstraction.

ts
// Before — raw HTML5 Drag & Drop
let enterCount = 0;
dropzone.addEventListener('dragenter', () => {
  enterCount++;
  dropzone.classList.add('over');
});
dropzone.addEventListener('dragleave', () => {
  if (--enterCount === 0) dropzone.classList.remove('over');
});
dropzone.addEventListener('dragover', (e) => e.preventDefault());
dropzone.addEventListener('drop', (e) => {
  e.preventDefault();
  enterCount = 0;
  const files = [...e.dataTransfer!.files];
  if (!files.every((f) => f.type.startsWith('image/'))) return showError('Images only');
  uploadFiles(files);
});

// After — Dnd
import { createDropZone } from '@vielzeug/dnd/drop';
const zone = createDropZone({
  element: dropzone,
  accept: ['image/*'],
  onDrop: (files) => uploadFiles(files),
  onDropRejected: (files) => showError(`${files.length} file(s) not accepted`),
  onHoverChange: (hovered) => dropzone.classList.toggle('over', hovered),
});
FeatureDNDSortableJSdnd-kit
Bundle size6.3 KB~15 kB~30 kB
Framework agnostic
MIME type filtering Pre-validated
Counter-based hoverN/A
Sortable lists
Drag handles
using support
Touch support Scoped opt-in
Zero third-party dependencies

Use Dnd when an item is picked up and dropped: file drop zones, sortable collections, keyboard reordering, or connected lists.

Use Gesture instead when a surface follows free or axis-locked pointer movement and application code owns rendering and completion, such as direct manipulation, swipe reveal, carousel navigation, or drawer dismissal.

Consider dnd-kit if you are building a React app and need complex multi-container drag interactions or accessibility-first sortable trees.

Installation

sh
pnpm add @vielzeug/dnd
sh
npm install @vielzeug/dnd
sh
yarn add @vielzeug/dnd

Quick Start

ts
import { createDropZone } from '@vielzeug/dnd/drop';
import { createSortable } from '@vielzeug/dnd/sortable';

// File drop zone — with async validation and paste support
const dropzone = document.getElementById('dropzone')!;

using zone = createDropZone({
  element: dropzone,
  accept: ['image/*', '.pdf'],
  paste: true,
  onValidate: (files) => files.every((file) => file.size <= 5_000_000),
  onDrop: (files) => console.log('Upload', files),
  onDropRejected: (files) => {
    console.warn(`${files.length} file(s) rejected`);
  },
  onHoverChange: (hovered) => {
    dropzone.classList.toggle('drag-over', hovered);
  },
});

// Sortable list — with application-owned rollback for optimistic updates
let currentOrder = ['a', 'b', 'c'];
const history: Array<{ before: readonly string[] }> = [];

using sortable = createSortable({
  element: document.getElementById('list')!,
  keyboard: true,
  onBeforeReorder: (from, to) => {
    // record positions here before the DOM commits (for FLIP animations)
  },
  getKey: (el) => el.dataset.sortId!,
  onReorder: ({ before, after }) => {
    history.push({ before });
    currentOrder = after;
  },
});

Features

  • Counter-based hover stateonHoverChange stays accurate when dragging over child elements; hover only activates when the drag payload passes the accept filter, with symmetric enter/leave pairing to prevent flicker
  • MIME type pre-validation — queries dataTransfer.items during drag to set dropEffect='none' before the drop; confirmed against File.type on drop
  • Flexible accept patterns — MIME types (image/png), wildcards (image/*), and file extensions (.pdf)
  • maxFiles limit — cap the number of accepted files per drop; excess files are forwarded to onDropRejected
  • onValidate async gating — optional cancellable async step after type filtering; zone.validating remains true until every pending validation settles
  • Clipboard paste supportpaste: true routes pasted files through the same accept, maxFiles, and onValidate pipeline; onPaste provides a separate callback; paste rejections are forwarded to onDropRejected with the same (files: File[]) => void signature as drop rejections
  • onDropRejected — separate callback for files that didn't match accept, exceeded maxFiles, or were rejected by onValidate
  • Sortable lists — reorders DOM children with a placeholder indicator; fires onReorder only when the order actually changes
  • Drag handles — scope dragging to a child selector via handle; whole item is draggable when omitted
  • Custom drag preview — pass an element or a (id, item, event) => element | null factory; control hotspot with dragImageOffset
  • onBeforeReorder FLIP hook — fires before commit for both drag and keyboard moves; pair it with captureLayout() for lifecycle-owned FLIP animation
  • Application-owned rollbackonReorder emits { before, after, item } so application history can own undo/rollback instead of the drag controller
  • Boundary-safe keyboard reordering — arrow keys at the first/last item no longer suppress preventDefault, so the browser can scroll the page normally
  • Transactional connected scopes — one onMove callback receives each cross-list transfer with both final orders
  • Scoped touch supportcreateSortableScope({ touch: true }) handles only items registered to that scope and uses an inert outline preview
  • Explicit DOM refresh — call sortable.refresh() after DOM mutations, or provide an items() provider for explicit item ownership
  • [Symbol.dispose] — both primitives support the using keyword for automatic cleanup
  • Reactive-friendly optionsdisabled is re-read on each event (reassign options.disabled = true to toggle); accept is normalized and snapshotted at construction; recreate the zone to change accepted types
  • Shared pointer recognition — touch sorting uses @vielzeug/gesture; Dnd retains previews, hit-testing, drag events, and sortable transactions

Documentation

See Also

  • Orbit — floating element positioning; use alongside Dnd to anchor drag previews and drop-zone indicators to precise positions
  • Gesture — pointer recognition used by Dnd touch sorting and directly by swipe, reveal, carousel, and drawer interactions
  • Ore — web-component authoring framework for application-owned draggable and sortable surfaces
  • Refine — accessible web components; Dnd powers Refine's file-drop input behavior