@shopify/draggable

A modular JavaScript drag-and-drop toolkit with composable Sortable, Swappable, and Droppable modules built on a common Draggable core.

Library
npm
v1.2.1
18,470stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
50/100Fair
Development Activity0
Maintenance20
Community80
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
81/100Excellent
Architecture78
Code Quality82
Innovation72
Learning Curve90

@shopify/draggable is a modular drag-and-drop toolkit, originally built by Shopify and now community-maintained, that abstracts native drag, mouse, touch, and force-touch events into a single, consistent, cancelable event API. Rather than shipping one opinionated widget, it exposes four composable modules — Draggable, Sortable, Swappable, and Droppable — plus a plugin and sensor system, so teams can build anything from a reorderable list to a custom Kanban board or a drop-target file uploader without wrestling with the quirks of the native HTML5 Drag and Drop API.

Under the hood, the core Draggable class does the heavy lifting: it manages sensors, emits a cancelable lifecycle of events (drag:start, drag:move, drag:over, drag:stop, and more), and creates the visual “mirror” element that follows the pointer during a drag. Sortable, Swappable, and Droppable are all built on top of it, giving each a familiar, consistent API while layering on module-specific behavior like list reordering or item swapping. Plugins such as Mirror, Scrollable, Focusable, and Announcement (for screen-reader accessibility) can be swapped in or out per instance.

What You Get

  • Four composable modules — Draggable (core), Sortable, Swappable, Droppable — each targeting a different interaction pattern
  • A sensor system (Mouse, Touch, Drag, Force Touch) that normalizes input across devices and browsers
  • A cancelable event lifecycle (drag:start, drag:move, drag:over/out, drag:stop) you can hook into or prevent
  • Built-in plugins for mirror rendering, auto-scrolling containers, focus management, and screen-reader announcements
  • First-class TypeScript type definitions shipped in the package, plus cjs/esm/esnext/UMD builds

Common Use Cases

  • Reorderable to-do lists, Kanban boards, and admin dashboards
  • Drag-to-swap image galleries or grid-based layouts
  • File-upload drop zones built on the Droppable module
  • Custom sortable form builders or page-layout editors

Under The Hood

Architecture Draggable’s core (src/Draggable/Draggable.js, ~736 lines) is a single class that wires together an Emitter (src/Draggable/Emitter), a set of Sensor instances (src/Draggable/Sensors — MouseSensor, TouchSensor, DragSensor, ForceTouchSensor), and a set of Plugins (Announcement, Focusable, Mirror, Scrollable), translating raw pointer/touch input into a cancelable stream of custom DragEvent/DraggableEvent objects. Sortable, Swappable, and Droppable (src/Sortable, src/Swappable, src/Droppable) don’t reimplement drag handling — they compose a Draggable instance internally and register their own module-specific plugins and sensors on top, giving each module a consistent, predictable event surface. Shared contracts live under src/shared: AbstractPlugin defines the attach()/detach() lifecycle every plugin must implement, and AbstractEvent defines the cancelable event base class every DragEvent/SortableEvent/etc. extends. This is a clean, layered, event-driven design that’s extensible without touching the core, though the core Draggable class itself still carries a fair amount of option-normalization and plugin-wiring responsibility.

Tech Stack The library is written in a TypeScript/JavaScript hybrid (a portion of the codebase has migrated to .ts, the rest remains ES6 classes), compiled via tsc + tsc-alias and bundled with Rollup (rollup.config.ts / rollup.development.config.ts) into parallel cjs, esm, esnext, and UMD builds, with uglify-js minifying the UMD bundle. Babel (babel.config.js) handles broader JS transpilation, and the package ships hand-written TypeScript declarations (index.d.ts) alongside compiled output. Tests run under Jest with jest-environment-jsdom to simulate DOM events. There are zero runtime dependencies — this is a pure browser library — and releases are versioned and published through Changesets, with GitHub Actions running lint, type-check, build, and test on every change.

Code Quality Each module (Draggable, Sortable, Swappable, Droppable) has its own tests directory with dedicated Jest suites plus shared test helpers and custom matchers (test/matchers, test/helpers) for simulating mouse/touch drag sequences and asserting on emitted events. Linting is enforced via @shopify/eslint-plugin’s TypeScript and Jest configs with zero tolerated warnings, and formatting via @shopify/prettier-config, both run in CI alongside strict type-checking. Error handling favors explicit, cancelable custom events over thrown exceptions for library-consumer-facing flow control (calling event.cancel() to stop a drag), while internal abstract methods throw explicit “Not Implemented” errors if a subclass fails to override them. Naming is consistent and class-based throughout, with documentation annotations covering the public API surface even in the untyped modules.

API Design The public API is intentionally small and composable: instantiate Draggable (or one of its three subclasses) against a container and a CSS selector, and behavior extends through static Plugins/Sensors namespaces and a documented event lifecycle (draggable:initialize, drag:start, drag:move, drag:over, drag:stop, draggable:destroy, plus module-specific events like sortable:sort or droppable:over) rather than through configuration flags. This composition-over-configuration model lets consumers replace or extend individual concerns — writing a custom Sensor or Plugin — without forking the library, and shipped TypeScript types make each event’s payload discoverable in editors. The tradeoff is a moderate onboarding curve: there’s no React/Vue-specific wrapper, so framework integration is left entirely to the consumer, and getting sortable behavior working requires understanding several related classes rather than a single drop-in component.

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