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 dependencies.
v0.0.13.7 KB gzip 0 depsBrowser
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';
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 size3.7 KB~15 kB~30 kB
Framework agnostic
MIME type filtering Pre-validated
Counter-based hoverN/A
Sortable lists
Drag handles
using support
Zero dependencies

Use Dnd when you need reliable file drop zones with MIME filtering or sortable lists in a framework-agnostic environment.

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, createSortable } from '@vielzeug/dnd';

// File drop zone — with async validation and paste support
using zone = createDropZone({
  element: document.getElementById('dropzone')!,
  accept: ['image/*', '.pdf'],
  paste: true,
  onValidate: async (files) => checkServerQuota(files),
  onDrop: (files) => uploadFiles(files),
  onDropRejected: (files) => {
    showError(`${files.length} file(s) not accepted`);
  },
  onHoverChange: (hovered) => {
    document.getElementById('dropzone')!.classList.toggle('drag-over', hovered);
  },
});

// Sortable list — with revert support for optimistic updates
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: ({ ids, setRevert }) => {
    const prev = currentOrder;
    setOrder(ids);
    setRevert(() => setOrder(prev)); // enable sortable.revert() on failure
  },
});

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 async step after type filtering; zone.validating is true while a promise is pending; only receives type-accepted files
  • 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; event type reflects whether the rejection came from a drop or a paste
  • 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; items are still in pre-commit positions, making it ideal for FLIP animation setup
  • sortable.revert() — register a revert function via event.setRevert(fn) inside onReorder; sortable.revert() invokes it and clears it for rolling back optimistic updates on server failure
  • Boundary-safe keyboard reordering — arrow keys at the first/last item no longer suppress preventDefault, so the browser can scroll the page normally
  • Explicit connected scopes — lists only exchange items when they share a createSortableScope() instance
  • Explicit DOM sync — call sortable.sync() after DOM mutations instead of relying on hidden observers
  • [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 captures the array reference, so push/splice mutations are reflected without recreating the zone
  • Zero dependencies3.7 KB gzipped, 0 dependencies

Documentation

See Also

  • Orbit — floating element positioning; use alongside Dnd to anchor drag previews and drop-zone indicators to precise positions
  • Craft — web-component authoring framework; build draggable custom elements with Dnd's pointer event primitives
  • Sigil — accessible web components; Dnd powers the drag-and-drop inside Sigil's sortable list and kanban components