svelte-dnd-action

A lightweight, action-based drag-and-drop library for Svelte with full keyboard, touch, and screen-reader accessibility support.

Library
npm
v0.9.79
2,124stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
73/100Good
Development Activity84
Maintenance52
Community56
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
69/100Good
Architecture78
Code Quality68
Innovation85
Learning Curve45

svelte-dnd-action is a zero-dependency Svelte action that turns any container into a fully-featured drag-and-drop zone. Rather than wrapping components in a higher-order component, it attaches directly to the DOM node via Svelte’s action API, giving fine-grained control over layout, styling, and animations without giving up the DOM structure you already have.

It supports nested drag-and-drop zones, horizontal and vertical lists, touch devices, and keyboard-only interaction, with automatic ARIA attributes and screen-reader announcements built in. The library has been used in production for several years, and the maintainer prioritizes API stability, so breaking changes are rare.

What You Get

  • Custom Svelte action (dndzone) that attaches to any container element, no wrapper components required
  • Full accessibility support including keyboard dragging, ARIA live-region announcements, and screen-reader instructions
  • Built-in touch device support with a configurable delayTouchStart to distinguish drags from scrolls
  • Nested drag-and-drop zones so containers-within-containers (Trello-style boards) work out of the box
  • Configurable zone “type” system to restrict which zones can exchange items with each other
  • Drag handle wrapper (dragHandleZone/dragHandle) for restricting the draggable trigger to part of an item

Common Use Cases

  • Kanban-style boards with draggable cards across multiple columns
  • Sortable to-do lists and task managers where users reorder items
  • Nested list builders (lists inside lists) such as outline or file-tree editors
  • Reorderable form builders where fields can be dragged into a new sequence
  • Custom drag handles for admin dashboards and playlist/queue reordering UIs

Under The Hood

Architecture dndzone in action.js delegates to two independent implementations - pointerDndZone (pointerAction.js, 718 lines) for mouse/touch, and keyboardDndZone (keyboardAction.js, 445 lines) for keyboard-driven dragging, both instantiated in parallel and merged into one action interface with update/destroy lifecycle methods (src/action.js). Shared low-level concerns are factored into src/helpers/: DOM cloning for the dragged element preview (svelteNodeClone.js), auto-scroll physics for both the dragged zone and ancestor scroll containers (scroller.js, multiScroller.js), custom event dispatch (dispatcher.js), collision/intersection math for determining drop index (intersection.js), inline style application for the placeholder and dragged clone (styler.js), and shared list-index utilities (listUtil.js). Accessibility is centralized in helpers/aria.js, which both pointerAction and keyboardAction call into to keep ARIA attributes and live-region announcements consistent regardless of input method. State communication back to the host component flows exclusively through two custom DOM events (consider/finalize) dispatched from the zone element, so the library owns no external state store - the host’s own item array is the single source of truth, fed back in through Svelte’s action update lifecycle. This event-only contract is what would break if changed: any component depending on dndzone assumes items flows in and modified items flows back out via those two events.

Tech Stack Pure vanilla JavaScript (ES2021, zero runtime dependencies) shipped as a dual ESM/CJS build (dist/index.mjs / dist/index.js) via Rollup (rollup.config.js, @rollup/plugin-node-resolve, rollup-plugin-babel, rollup-plugin-copy), transpiled with Babel (@babel/core, @babel/preset-env) targeting browser environments. The only external contract is a Svelte peer dependency (svelte >=3.23.0 || ^5.0.0-next.0), and the package exposes a svelte field pointing straight at src/index.js so bundlers that understand Svelte can consume the untranspiled action source directly. TypeScript consumers get typings from a hand-authored dist/index.d.ts (source in typings/). Linting is ESLint (eslint:recommended) plus Prettier for formatting, wired together with Husky + lint-staged as a pre-commit hook. Tests run through Cypress rather than a typical unit-test runner, executing spec files directly against built DOM behavior.

Code Quality Testing uses Cypress specs under cypress/integration/, with one spec file per source module (pointerAction, keyboardAction, aria, dispatcher, intersection, listUtil, styler, util, dragHandleZone, keyboardDragTrigger) - real DOM-based tests rather than pure unit mocks, appropriate for a library whose core logic is DOM geometry and event handling. Error handling favors explicit validation over silent failure: action.js’s validateOptions throws on malformed options rather than defaulting silently. Naming is descriptive and consistent, and JSDoc typedefs annotate the public Options shape even though the codebase itself is untyped JavaScript (types are hand-maintained separately in typings/, a manual sync risk). ESLint + Prettier + Husky pre-commit hooks enforce style, but no CI workflow files are present in the repository itself, so test execution on PRs isn’t verifiable from the repo alone.

API Design The entire public surface is a single action (use:dndzone={options}) plus two custom events (on:consider, on:finalize), which means adopting the library requires no new component tree, no render props, and no HOC wrapping - existing #each blocks keep working with directives simply added to the container, an unusually low-boilerplate integration. Advanced behavior (copy-on-drag, custom placeholders, translated ARIA strings) is opt-in via named exports (transformDraggedElement, setAriaStrings, SHADOW_ITEM_MARKER_PROPERTY_NAME) rather than crammed into the base API, keeping the common case simple while exposing escape hatches. The README documents every option in a single table with type/default/description columns and links to live Svelte REPL playgrounds for advanced recipes (nested zones, drag handles, custom placeholder styling) instead of just prose, lowering the cost of understanding non-trivial behavior. The main friction point is the info-object shape returned on consider/finalize (trigger/id/source from exported TRIGGERS/SOURCES enums), which requires reading the README closely, and the fact that pointer vs keyboard behavior is silently unified behind one action, so debugging a failure on one input path requires knowing a second parallel implementation exists.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search