Install
openclaw skills install shopify-draggableImplement drag-and-drop interactions with @shopify/draggable. Supports Draggable (basic drag), Sortable (reordering), Droppable (drop zones), Swappable (swapping), Plugins (mirror/snapping/collision/scroll, etc.), Sensors (mouse/touch/force touch).
openclaw skills install shopify-draggableBuilt 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.
npm install @shopify/draggable
Or via CDN (recommended for prototyping):
<script type="module">
import { Draggable, Sortable, Droppable, Swappable } from 'https://cdn.jsdelivr.net/npm/@shopify/draggable/build/esm/index.mjs';
</script>
| Module | Class | Purpose |
|---|---|---|
| Base | Draggable | Core drag engine, manages mirror, sensors, and events |
| Sorting | Sortable | Reorder on drag, tracks old and new indices |
| Drop Zone | Droppable | Drag elements into/out of specific dropzones |
| Swap | Swappable | Swap two element positions on drag (no sorting) |
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');
});
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}`);
});
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');
});
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);
});
| Option | Type | Default | Description |
|---|---|---|---|
draggable | String | '.draggable-source' | CSS selector for draggable elements |
handle | String|null | null | CSS selector for drag handle |
delay | Object | {} | Delay before drag starts ({ mouse: ms, touch: ms }) |
distance | Number | 0 | Minimum pixels to move before dragging |
placedTimeout | Number | 800 | Delay before removing placed CSS classes (ms) |
plugins | Array | [] | Additional plugins |
sensors | Array | [] | Additional sensors |
classes | Object | see below | Custom CSS class names |
announcements | Object | see below | Accessibility announcements |
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:
| Identifier | Default Class | Description |
|---|---|---|
droppable:active | draggable-dropzone--active | Accepting drop zones |
droppable:occupied | draggable-dropzone--occupied | Occupied drop zones |
new Draggable(containers, {
exclude: {
plugins: [], // List of plugin constructors
sensors: [], // List of sensor constructors
},
});
| Event | Trigger |
|---|---|
drag:start | Drag started |
drag:move | Drag moving |
drag:over | Hovering over another draggable element |
drag:over:container | Hovering over another container |
drag:out | Moved out of an element |
drag:out:container | Moved out of a container |
drag:stop | Drag stopped |
drag:pressure | Pressure change (Force Touch) |
drag:stopped | Drag fully ended |
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
});
| Event | Additional Properties |
|---|---|
sortable:start | startIndex, startContainer |
sortable:sort | currentIndex, source, over |
sortable:sorted | oldIndex, newIndex, oldContainer, newContainer |
sortable:stop | oldIndex, newIndex, oldContainer, newContainer |
| Event | Additional Properties |
|---|---|
droppable:start | dragEvent, dropzone |
droppable:dropped | dragEvent, dropzone |
droppable:returned | dragEvent, dropzone |
droppable:stop | dragEvent, dropzone |
| Event | Additional Properties |
|---|---|
swappable:start | dragEvent |
swappable:swap | dragEvent, over, overContainer |
swappable:swapped | dragEvent, swappedElement |
swappable:stop | dragEvent |
| Plugin | Class | Function |
|---|---|---|
| Announcement | Draggable.Plugins.Announcement | Live accessibility announcements during drag |
| Focusable | Draggable.Plugins.Focusable | Keyboard focus management |
| Mirror | Draggable.Plugins.Mirror | Shows mirror element while dragging (enabled by default) |
| Scrollable | Draggable.Plugins.Scrollable | Auto-scroll container when dragging near edge |
| Collidable | Plugins.Collidable | Collision detection (requires separate import) |
| ResizeMirror | Plugins.ResizeMirror | Auto-resize mirror element |
| Snappable | Plugins.Snappable | Snap to specific positions |
| SwapAnimation | Plugins.SwapAnimation | Swap animation |
| SortAnimation | Plugins.SortAnimation | Sort animation |
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).
import Snappable from '@shopify/draggable/build/esm/Plugins/Snappable';
new Draggable(containers, {
plugins: [Snappable],
});
| Sensor | Description | Default |
|---|---|---|
MouseSensor | Mouse drag | ✅ |
TouchSensor | Touch drag | ✅ |
ForceTouchSensor | Force Touch pressure | ❌ |
DragSensor | Native HTML5 Drag & Drop | ❌ |
import { Draggable } from '@shopify/draggable';
import ForceTouchSensor from '@shopify/draggable/build/esm/Draggable/Sensors/ForceTouchSensor';
new Draggable(containers, {
sensors: [ForceTouchSensor],
});
| Method | Description |
|---|---|
addPlugin(...plugins) | Add plugins |
removePlugin(...plugins) | Remove plugins |
addSensor(...sensors) | Add sensors |
removeSensor(...sensors) | Remove sensors |
addContainer(...containers) | Dynamically add containers |
removeContainer(...containers) | Dynamically remove containers |
on(type, ...callbacks) | Bind event listener |
off(type, callback) | Unbind event listener |
trigger(event) | Trigger an event |
isDragging() | Check if currently dragging |
getDraggableElements() | Get all draggable elements |
cancel() | Immediately cancel current drag |
destroy() | Destroy the instance |
new Sortable(document.querySelectorAll('.list'), {
draggable: '.list-item',
handle: '.drag-handle', // Only .drag-handle can trigger drag
});
new Draggable(containers, {
delay: { mouse: 100, touch: 200 },
distance: 5, // Must move 5px before dragging
});
const sortable = new Sortable(document.querySelectorAll('.column'), {
draggable: '.card',
delay: { touch: 200 },
});
// Cross-container drag is supported automatically
draggable.on('drag:start', (event) => {
if (event.source.dataset.draggable === 'false') {
event.cancel();
}
});
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); }
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