@shopify/draggable — Drag & Drop Interaction Skill

Other

Implement 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).

Install

openclaw skills install shopify-draggable

@shopify/draggable — Drag & Drop Interaction Skill

Built on Shopify's @shopify/draggable (MIT license, v1.2.1). Pure JS library, zero runtime dependencies, supports ES modules and UMD.

Use cases

sortable lists, droppable panels, custom drag interactions, kanban/grid drag-and-drop reorganization. No longer maintained, now community-maintained.

Install

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 Overview

ModuleClassPurpose
BaseDraggableCore drag engine, manages mirror, sensors, and events
SortingSortableReorder on drag, tracks old and new indices
Drop ZoneDroppableDrag elements into/out of specific dropzones
SwapSwappableSwap two element positions on drag (no sorting)

Quick Start

Basic Drag

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');
});

Sortable List

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}`);
});

Drag into Drop Zone

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');
});

Element Swap

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);
});

Configuration Options

OptionTypeDefaultDescription
draggableString'.draggable-source'CSS selector for draggable elements
handleString|nullnullCSS selector for drag handle
delayObject{}Delay before drag starts ({ mouse: ms, touch: ms })
distanceNumber0Minimum pixels to move before dragging
placedTimeoutNumber800Delay before removing placed CSS classes (ms)
pluginsArray[]Additional plugins
sensorsArray[]Additional sensors
classesObjectsee belowCustom CSS class names
announcementsObjectsee belowAccessibility announcements

CSS Class Map

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 ClassDescription
droppable:activedraggable-dropzone--activeAccepting drop zones
droppable:occupieddraggable-dropzone--occupiedOccupied drop zones

Excluding Default Plugins/Sensors

new Draggable(containers, {
  exclude: {
    plugins: [],     // List of plugin constructors
    sensors: [],     // List of sensor constructors
  },
});

Events API

Base Events (All Modules)

EventTrigger
drag:startDrag started
drag:moveDrag moving
drag:overHovering over another draggable element
drag:over:containerHovering over another container
drag:outMoved out of an element
drag:out:containerMoved out of a container
drag:stopDrag stopped
drag:pressurePressure change (Force Touch)
drag:stoppedDrag fully ended

Common Event Properties

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
});

Sortable Events

EventAdditional Properties
sortable:startstartIndex, startContainer
sortable:sortcurrentIndex, source, over
sortable:sortedoldIndex, newIndex, oldContainer, newContainer
sortable:stopoldIndex, newIndex, oldContainer, newContainer

Droppable Events

EventAdditional Properties
droppable:startdragEvent, dropzone
droppable:droppeddragEvent, dropzone
droppable:returneddragEvent, dropzone
droppable:stopdragEvent, dropzone

Swappable Events

EventAdditional Properties
swappable:startdragEvent
swappable:swapdragEvent, over, overContainer
swappable:swappeddragEvent, swappedElement
swappable:stopdragEvent

Plugins

PluginClassFunction
AnnouncementDraggable.Plugins.AnnouncementLive accessibility announcements during drag
FocusableDraggable.Plugins.FocusableKeyboard focus management
MirrorDraggable.Plugins.MirrorShows mirror element while dragging (enabled by default)
ScrollableDraggable.Plugins.ScrollableAuto-scroll container when dragging near edge
CollidablePlugins.CollidableCollision detection (requires separate import)
ResizeMirrorPlugins.ResizeMirrorAuto-resize mirror element
SnappablePlugins.SnappableSnap to specific positions
SwapAnimationPlugins.SwapAnimationSwap animation
SortAnimationPlugins.SortAnimationSort animation

Collidable

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).

Snappable

import Snappable from '@shopify/draggable/build/esm/Plugins/Snappable';
new Draggable(containers, {
  plugins: [Snappable],
});

Sensors

SensorDescriptionDefault
MouseSensorMouse drag
TouchSensorTouch drag
ForceTouchSensorForce Touch pressure
DragSensorNative HTML5 Drag & Drop
import { Draggable } from '@shopify/draggable';
import ForceTouchSensor from '@shopify/draggable/build/esm/Draggable/Sensors/ForceTouchSensor';

new Draggable(containers, {
  sensors: [ForceTouchSensor],
});

Instance Methods

MethodDescription
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

Common Patterns

Drag with Handle

new Sortable(document.querySelectorAll('.list'), {
  draggable: '.list-item',
  handle: '.drag-handle',   // Only .drag-handle can trigger drag
});

Delay & Minimum Distance (Prevent Accidental Drag)

new Draggable(containers, {
  delay: { mouse: 100, touch: 200 },
  distance: 5,            // Must move 5px before dragging
});

Cross-Container Sorting

const sortable = new Sortable(document.querySelectorAll('.column'), {
  draggable: '.card',
  delay: { touch: 200 },
});
// Cross-container drag is supported automatically

Prevent Specific Drags

draggable.on('drag:start', (event) => {
  if (event.source.dataset.draggable === 'false') {
    event.cancel();
  }
});

Custom Mirror Style

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); }

Lifecycle

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()

Notes

  • Draggable does not perform sorting by itself — Sortable, Droppable, and Swappable are its subclasses
  • The source element is set to display: none during drag; the mirror takes its visual place
  • No longer maintained by the original Shopify authors; now community-maintained. Evaluate risk for production use
  • TypeScript type definitions are bundled — no need to install @types separately
  • Does not support IE11; targets ES6 modern browsers
  • Use jsdelivr for CDN; avoid unpkg (incompatible paths)