@neodrag/react

A lightweight React hook that makes any DOM element draggable, powered by the tiny, framework-agnostic @neodrag/core engine.

Library
npm
v2.3.1
2,451stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
56/100Fair
Development Activity36
Maintenance48
Community44
Maturity56
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
71/100Good
Architecture78
Code Quality42
Innovation80
Learning Curve85

@neodrag/react is the React binding for Neodrag, a multi-framework dragging library that ships identical drag behavior across Svelte, Vue, Solid, React, and vanilla JS. Rather than reimplementing drag logic per framework, it wraps the shared @neodrag/core engine in a single useDraggable hook, giving React apps axis locking, bounds constraints, custom handles/cancel targets, GPU-accelerated transforms, and live drag state in a package that compiles to just a couple of kilobytes.

The hook is deliberately minimal: pass it a ref and an options object, and it wires up pointer listeners, applies transforms, and exposes isDragging and dragState for rendering feedback. Options can be changed on the fly and the hook re-syncs the underlying engine without re-mounting, making it a drop-in replacement for heavier drag libraries like react-draggable while staying framework-agnostic at its core.

What You Get

  • A useDraggable(ref, options) hook that attaches drag behavior to any HTML element via a ref
  • Live isDragging and dragState (offsetX, offsetY, DOMRect) values for building custom UI feedback while dragging
  • Axis locking (x, y, both, none) and bounds constraints (parent, body, a selector, or explicit coordinates)
  • Flexible handle/cancel targeting that accepts a selector string, an HTMLElement, a React ref, or an array of either
  • Reactive options — changing the options object updates the drag behavior in place without unmounting
  • Full TypeScript types (DragOptions, DragEventData, DragAxis, DragBounds, DragBoundsCoords) re-exported from @neodrag/core

Common Use Cases

  • Draggable modals, dialogs, and floating panels that users can reposition on screen
  • Custom drag-and-drop widgets (kanban cards, movable dashboard tiles) where a full DnD library is overkill
  • Draggable sliders, handles, or resize grips constrained to a parent container
  • Free-form canvas or whiteboard-style UIs where elements can be moved anywhere within bounds
  • Migrating off react-draggable for a smaller bundle while keeping a familiar drag API

Under The Hood

Architecture The react package is a thin, single-file adapter (packages/react/src/index.ts) around the framework-agnostic draggable() function exported by @neodrag/core. The useDraggable hook holds the core engine’s update/destroy handles in a ref, wraps the core’s onDragStart/onDrag/onDragEnd callbacks to also flow into React state (isDragging, dragState), and normalizes React-specific handle/cancel inputs (strings, elements, refs, or arrays of refs) into the plain DOM values core expects via an unwrap_handle_cancel helper. A mount-time effect initializes the engine once against the ref’d node and tears it down on unmount; a second effect calls the engine’s update() whenever the options object changes, so the actual bounds/axis/transform logic never has to be reimplemented for React — it is entirely owned by @neodrag/core, and this same adapter pattern is mirrored nearly identically across the Svelte, Vue, Solid, and vanilla packages in the monorepo.

Tech Stack The monorepo is a pnpm workspace (pnpm-workspace.yaml) with per-package tsup builds emitting ESM output plus a minified production build (dist/min), TypeScript throughout, and Changesets (@changesets/cli + @changesets/changelog-github) driving versioning and GitHub-integrated changelogs via ci:version/ci:release scripts. The react package itself declares no runtime dependencies beyond @neodrag/core (workspace-linked), targets React 18 as an implicit peer, and relies on native PointerEvents rather than a synthetic gesture library. GitHub Actions workflows (release.yml, cr.yml) handle publishing and changeset checks; vitest and jsdom are present at the root for testing, though only the Svelte package currently has spec files.

Code Quality No test files exist for the React package (or for @neodrag/core directly) — the monorepo’s only .spec.ts files live under packages/svelte/tests/, so the React hook’s correctness currently relies on manual testing and the shared core engine’s own use across the other framework packages. Typing is thorough: DragOptions, DragEventData, and related types are fully annotated with TSDoc comments and re-exported for consumers. Naming is consistent (snake_case for internal helpers/handlers, camelCase for public API), and a root .prettierrc enforces formatting, but there is no visible ESLint config or CI job that runs a test suite on this package.

API Design The public surface is intentionally tiny — one hook, useDraggable(ref, options), returning { isDragging, dragState } — with the same option shape (axis, bounds, handle, cancel, onDrag* callbacks) mirrored across every framework binding in the monorepo, so switching from Svelte or Vue to React means no relearning. Handle/cancel targets accept plain selectors, elements, refs, or arrays of either without extra boilerplate, and options are fully reactive — no need to re-mount to change drag behavior. The README explicitly positions it as a smaller (“3.7x smaller”), API-compatible alternative to react-draggable, and the demo apps under packages/react/demo/ double as living usage examples.

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