Built on Shopify's @shopify/draggable (MIT license, v1.2.1). Pure JS library, zero runtime dependencies, supports ES modules and UMD.
sortable lists, droppable panels, custom drag interactions, kanban/grid drag-and-drop reorganization. No longer maintained, now community-maintained.
bash
npm install @shopify/draggable
Or via CDN (recommended for prototyping):
html
<script type="module">
import { Draggable, Sortable, Droppable, Swappable } from 'https://cdn.jsdelivr.net/npm/@shopify/draggable/build/esm/index.mjs';
</script>
ModuleClassPurposeBaseDraggableCore drag engine, manages mirror, sensors, and eventsSortingSortableReorder on drag, tracks old and new indicesDrop ZoneDroppableDrag elements into/out of specific dropzonesSwapSwappableSwap two element positions on drag (no sorting)
javascript
import Draggable from '@shopify/draggable';
const draggable = new Draggable(document.querySelectorAll('.container'), {
draggable: '.draggable-source',
});
draggable.on('drag:start', (event) => {
console.log('Started dragging:', event.source);
});
draggable.on('drag:stop', () => {
console.log('Drag ended');
});
javascript
import Sortable from '@shopify/draggable';
const sortable = new Sortable(document.querySelectorAll('.list'), {
draggable: '.list-item',
delay: { mouse: 200, touch: 300 },
});
sortable.on('sortable:stop', (event) => {
console.log(`Moved from index ${event.oldIndex} to ${event.newIndex}`);
});
javascript
import Droppable from '@shopify/draggable';
const droppable = new Droppable(document.querySelectorAll('.source-container'), {
draggable: '.card',
dropzone: '.dropzone',
});
droppable.on('droppable:dropped', (event) => {
console.log('Dropped into dropzone:', event.dropzone);
});
droppable.on('droppable:returned', (event) => {
console.log('Returned to original position');
});
javascript
import Swappable from '@shopify/draggable';
const swappable = new Swappable(document.querySelectorAll('.grid'), {
draggable: '.grid-item',
});
swappable.on('swappable:swapped', (event) => {
console.log('Swapped element:', event.swappedElement);
});
OptionTypeDefaultDescriptiondraggable``String``'.draggable-source'CSS selector for draggable elementshandle``String|null``nullCSS selector for drag handledelay``Object``{}Delay before drag starts ( { mouse: ms, touch: ms })distance``Number``0Minimum pixels to move before draggingplacedTimeout``Number``800Delay before removing placed CSS classes (ms)plugins``Array``[]Additional pluginssensors``Array``[]Additional sensorsclasses``Objectsee belowCustom CSS class namesannouncements``Objectsee belowAccessibility announcements
javascript
const defaultClasses = {
'container:dragging': 'draggable-container--is-dragging',
'source:dragging': 'draggable-source--is-dragging',
'source:placed': 'draggable-source--placed',
'container:placed': 'draggable-container--placed',
'body:dragging': 'draggable--is-dragging',
'draggable:over': 'draggable--over',
'container:over': 'draggable-container--over',
'source:original': 'draggable--original',
mirror: 'draggable-mirror',
};
Droppable additional classes:
IdentifierDefault ClassDescriptiondroppable:active``draggable-dropzone--activeAccepting drop zonesdroppable:occupied``draggable-dropzone--occupiedOccupied drop zones
javascript
new Draggable(containers, {
exclude: {
plugins: [], // List of plugin constructors
sensors: [], // List of sensor constructors
},
});
EventTriggerdrag:startDrag starteddrag:moveDrag movingdrag:overHovering over another draggable elementdrag:over:containerHovering over another containerdrag:outMoved out of an elementdrag:out:containerMoved out of a containerdrag:stopDrag stoppeddrag:pressurePressure change (Force Touch)drag:stoppedDrag fully ended
javascript
draggable.on('drag:start', (event) => {
event.source; // Cloned source element
event.originalSource; // Original element (display:none)
event.sourceContainer; // Source container
event.sensorEvent; // Original sensor event
event.cancel(); // Cancel the drag
});
EventAdditional Propertiessortable:start``startIndex, startContainer``sortable:sort``currentIndex, source, over``sortable:sorted``oldIndex, newIndex, oldContainer, newContainer``sortable:stop``oldIndex, newIndex, oldContainer, newContainer
EventAdditional Propertiesdroppable:start``dragEvent, dropzone``droppable:dropped``dragEvent, dropzone``droppable:returned``dragEvent, dropzone``droppable:stop``dragEvent, dropzone
EventAdditional Propertiesswappable:start``dragEvent``swappable:swap``dragEvent, over, overContainer``swappable:swapped``dragEvent, swappedElement``swappable:stop``dragEvent
PluginClassFunctionAnnouncementDraggable.Plugins.AnnouncementLive accessibility announcements during dragFocusableDraggable.Plugins.FocusableKeyboard focus managementMirrorDraggable.Plugins.MirrorShows mirror element while dragging (enabled by default)ScrollableDraggable.Plugins.ScrollableAuto-scroll container when dragging near edgeCollidablePlugins.CollidableCollision detection (requires separate import)ResizeMirrorPlugins.ResizeMirrorAuto-resize mirror elementSnappablePlugins.SnappableSnap to specific positionsSwapAnimationPlugins.SwapAnimationSwap animationSortAnimationPlugins.SortAnimationSort animation
javascript
import { Plugins } from '@shopify/draggable';
import Collidable from '@shopify/draggable/build/esm/Plugins/Collidable';
new Draggable(containers, {
plugins: [Collidable],
});
Collidable events: collidable:in (entered collision), collidable:out (left collision).
javascript
import Snappable from '@shopify/draggable/build/esm/Plugins/Snappable';
new Draggable(containers, {
plugins: [Snappable],
});
SensorDescriptionDefaultMouseSensorMouse drag✅TouchSensorTouch drag✅ForceTouchSensorForce Touch pressure❌DragSensorNative HTML5 Drag & Drop❌javascript
import { Draggable } from '@shopify/draggable';
import ForceTouchSensor from '@shopify/draggable/build/esm/Draggable/Sensors/ForceTouchSensor';
new Draggable(containers, {
sensors: [ForceTouchSensor],
});
MethodDescriptionaddPlugin(...plugins)Add pluginsremovePlugin(...plugins)Remove pluginsaddSensor(...sensors)Add sensorsremoveSensor(...sensors)Remove sensorsaddContainer(...containers)Dynamically add containersremoveContainer(...containers)Dynamically remove containerson(type, ...callbacks)Bind event listeneroff(type, callback)Unbind event listenertrigger(event)Trigger an eventisDragging()Check if currently dragginggetDraggableElements()Get all draggable elementscancel()Immediately cancel current dragdestroy()Destroy the instance
javascript
new Sortable(document.querySelectorAll('.list'), {
draggable: '.list-item',
handle: '.drag-handle', // Only .drag-handle can trigger drag
});
javascript
new Draggable(containers, {
delay: { mouse: 100, touch: 200 },
distance: 5, // Must move 5px before dragging
});
javascript
const sortable = new Sortable(document.querySelectorAll('.column'), {
draggable: '.card',
delay: { touch: 200 },
});
// Cross-container drag is supported automatically
javascript
draggable.on('drag:start', (event) => {
if (event.source.dataset.draggable === 'false') {
event.cancel();
}
});
javascript
import Draggable from '@shopify/draggable';
const draggable = new Draggable(containers, {
classes: {
mirror: 'my-custom-mirror',
},
});
// Or customize via CSS
// .draggable-mirror { opacity: 0.7; transform: scale(1.05); }
text
constructor()
↓
draggable:initialized
↓
drag:start ──→ drag:move ──→ drag:stop ──→ drag:stopped
↓ ↓ ↓
Plugin events sortable:sorted sortable:stop
↓ droppable: droppable:stop
cancel() dropped swappable:stop
swappable:
swapped
↓
destroy()
display: none during drag; the mirror takes its visual place@types separately