The JavaScript Drag & Drop library your grandparents warned you about.
—
Plugins extend draggable functionality through an extensible architecture. Shopify Draggable includes built-in plugins for mirrors, scrolling, accessibility, focus management, collision detection, and animations, plus supports custom plugin development.
All plugins extend AbstractPlugin which provides the foundation for plugin functionality.
/**
* Base class for all draggable plugins
*/
abstract class AbstractPlugin {
constructor(draggable: Draggable);
protected abstract attach(): void;
protected abstract detach(): void;
}
export {AbstractPlugin as BasePlugin};Usage Example:
import { AbstractPlugin } from "@shopify/draggable";
class CustomPlugin extends AbstractPlugin {
attach() {
this.draggable.on('drag:start', this.onDragStart);
this.draggable.on('drag:stop', this.onDragStop);
}
detach() {
this.draggable.off('drag:start', this.onDragStart);
this.draggable.off('drag:stop', this.onDragStop);
}
onDragStart = (event) => {
// Custom drag start logic
};
onDragStop = (event) => {
// Custom drag stop logic
};
}Default plugins included with every Draggable instance.
class Draggable {
static Plugins: {
Announcement: typeof Announcement;
Focusable: typeof Focusable;
Mirror: typeof Mirror;
Scrollable: typeof Scrollable;
};
}Provides accessibility announcements for screen readers.
class Announcement extends AbstractPlugin {
options: AnnouncementOptions;
protected attach(): void;
protected detach(): void;
}
interface AnnouncementOptions {
expire: number;
[key: string]: string | (() => string) | number;
}Usage Example:
const draggable = new Draggable(containers, {
announcements: {
'drag:start': (event) => `Started dragging ${event.source.textContent}`,
'drag:stop': (event) => `Stopped dragging ${event.source.textContent}`,
expire: 10000 // 10 seconds
}
});Manages focus during drag operations for keyboard accessibility.
class Focusable extends AbstractPlugin {
getElements(): HTMLElement[];
protected attach(): void;
protected detach(): void;
}Creates a visual representation (ghost/mirror) of the dragged element.
class Mirror extends AbstractPlugin {
getElements(): HTMLElement[];
protected attach(): void;
protected detach(): void;
}
interface MirrorOptions {
xAxis?: boolean;
yAxis?: boolean;
constrainDimensions?: boolean;
cursorOffsetX?: number;
cursorOffsetY?: number;
appendTo?: string | HTMLElement | ((source: HTMLElement) => HTMLElement);
}Usage Example:
const draggable = new Draggable(containers, {
mirror: {
constrainDimensions: true,
cursorOffsetX: 10,
cursorOffsetY: 10,
appendTo: document.body
}
});
// Listen to mirror events
draggable.on('mirror:created', (event) => {
console.log('Mirror created:', event.mirror);
event.mirror.style.opacity = '0.8';
});Provides auto-scrolling when dragging near container edges.
class Scrollable extends AbstractPlugin {
protected attach(): void;
protected detach(): void;
}
interface ScrollableOptions {
speed?: number;
sensitivity?: number;
scrollableElements?: HTMLElement[];
}Usage Example:
const draggable = new Draggable(containers, {
scrollable: {
speed: 20,
sensitivity: 40,
scrollableElements: [document.documentElement]
}
});Extended plugins available through the Plugins export.
const Plugins: {
Collidable: typeof Collidable;
SwapAnimation: typeof SwapAnimation;
SortAnimation: typeof SortAnimation;
ResizeMirror: typeof ResizeMirror;
Snappable: typeof Snappable;
};Detects collisions between dragged elements and other elements.
class Collidable extends AbstractPlugin {
protected attach(): void;
protected detach(): void;
}
type Collidables = string | NodeList | HTMLElement[] | (() => NodeList | HTMLElement[]);
// Events
class CollidableEvent extends AbstractEvent {
readonly dragEvent: DragEvent;
readonly collidingElement: HTMLElement;
}
class CollidableInEvent extends CollidableEvent {}
class CollidableOutEvent extends CollidableEvent {}
type CollidableEventNames = 'collidable:in' | 'collidable:out';Usage Example:
import { Draggable, Plugins } from "@shopify/draggable";
const draggable = new Draggable(containers, {
plugins: [Plugins.Collidable],
collidables: '.obstacle, .barrier'
});
draggable.on('collidable:in', (event) => {
console.log('Collision detected with:', event.collidingElement);
event.collidingElement.classList.add('collision-active');
});
draggable.on('collidable:out', (event) => {
event.collidingElement.classList.remove('collision-active');
});Provides snap-to-grid or snap-to-element functionality.
class Snappable extends AbstractPlugin {
protected attach(): void;
protected detach(): void;
}
// Events
class SnapEvent extends AbstractEvent {
readonly dragEvent: DragEvent;
readonly snappable: HTMLElement;
}
class SnapInEvent extends SnapEvent {}
class SnapOutEvent extends SnapEvent {}
type SnappableEventNames = 'snap:in' | 'snap:out';Automatically resizes the mirror to match the drop target.
class ResizeMirror extends AbstractPlugin {
protected attach(): void;
protected detach(): void;
}Provides smooth animations for element swapping.
class SwapAnimation extends AbstractPlugin {
protected attach(): void;
protected detach(): void;
}
interface SwapAnimationOptions {
duration: number;
easingFunction: string;
horizontal: boolean;
}Provides smooth animations for sorting operations.
class SortAnimation extends AbstractPlugin {
protected attach(): void;
protected detach(): void;
}
interface SortAnimationOptions {
duration?: number;
easingFunction?: string;
}Animation Plugin Example:
import { Sortable, Plugins } from "@shopify/draggable";
const sortable = new Sortable(containers, {
plugins: [Plugins.SortAnimation],
sortAnimation: {
duration: 300,
easingFunction: 'ease-in-out'
}
});Methods for dynamically managing plugins on draggable instances.
/**
* Add plugins to a draggable instance
* @param plugins - Plugin classes to add
*/
addPlugin(...plugins: (typeof AbstractPlugin)[]): this;
/**
* Remove plugins from a draggable instance
* @param plugins - Plugin classes to remove
*/
removePlugin(...plugins: (typeof AbstractPlugin)[]): this;Usage Example:
const draggable = new Draggable(containers);
// Add plugins after initialization
draggable.addPlugin(Plugins.Collidable, Plugins.Snappable);
// Remove default plugins
draggable.removePlugin(Draggable.Plugins.Mirror);
// Exclude plugins during initialization
const draggable2 = new Draggable(containers, {
exclude: {
plugins: [Draggable.Plugins.Scrollable]
}
});class LoggingPlugin extends AbstractPlugin {
constructor(draggable) {
super(draggable);
this.logCount = 0;
}
attach() {
// Add event listeners
this.draggable.on('drag:start', this.onDragStart);
this.draggable.on('drag:move', this.onDragMove);
this.draggable.on('drag:stop', this.onDragStop);
console.log('LoggingPlugin attached');
}
detach() {
// Remove event listeners
this.draggable.off('drag:start', this.onDragStart);
this.draggable.off('drag:move', this.onDragMove);
this.draggable.off('drag:stop', this.onDragStop);
console.log('LoggingPlugin detached');
}
onDragStart = (event) => {
this.logCount++;
console.log(`Drag #${this.logCount} started:`, {
source: event.source.id || event.source.className,
timestamp: Date.now()
});
};
onDragMove = (event) => {
console.log('Drag move:', {
x: event.sensorEvent.clientX,
y: event.sensorEvent.clientY
});
};
onDragStop = (event) => {
console.log(`Drag #${this.logCount} ended:`, {
duration: Date.now() - this.startTime,
finalPosition: {
x: event.sensorEvent.clientX,
y: event.sensorEvent.clientY
}
});
};
}
// Use custom plugin
const draggable = new Draggable(containers, {
plugins: [LoggingPlugin]
});class SmartMirrorPlugin extends AbstractPlugin {
constructor(draggable) {
super(draggable);
this.mirrors = new Map();
}
attach() {
this.draggable.on('mirror:create', this.onMirrorCreate);
this.draggable.on('mirror:created', this.onMirrorCreated);
this.draggable.on('drag:over', this.onDragOver);
this.draggable.on('mirror:destroy', this.onMirrorDestroy);
}
detach() {
this.draggable.off('mirror:create', this.onMirrorCreate);
this.draggable.off('mirror:created', this.onMirrorCreated);
this.draggable.off('drag:over', this.onDragOver);
this.draggable.off('mirror:destroy', this.onMirrorDestroy);
}
onMirrorCreate = (event) => {
// Enhance mirror creation
console.log('Creating smart mirror for:', event.source);
};
onMirrorCreated = (event) => {
const mirror = event.mirror;
const source = event.source;
// Store mirror reference
this.mirrors.set(source, mirror);
// Add smart features
mirror.classList.add('smart-mirror');
mirror.style.boxShadow = '0 5px 15px rgba(0,0,0,0.3)';
mirror.style.transform = 'rotate(5deg) scale(1.05)';
// Add drag count badge
const badge = document.createElement('div');
badge.className = 'drag-count-badge';
badge.textContent = (this.dragCount || 0) + 1;
mirror.appendChild(badge);
};
onDragOver = (event) => {
const mirror = this.mirrors.get(event.originalSource);
if (!mirror) return;
// Change mirror appearance based on drop target
if (event.over) {
mirror.style.borderColor = event.over.dataset.category === 'valid' ? 'green' : 'red';
mirror.style.opacity = '1';
} else {
mirror.style.borderColor = 'transparent';
mirror.style.opacity = '0.8';
}
};
onMirrorDestroy = (event) => {
const source = event.originalSource;
this.mirrors.delete(source);
this.dragCount = (this.dragCount || 0) + 1;
};
}
// Use the smart mirror plugin
const draggable = new Draggable(containers, {
plugins: [SmartMirrorPlugin]
});// Configure built-in plugins
const draggable = new Draggable(containers, {
// Mirror configuration
mirror: {
constrainDimensions: true,
xAxis: true,
yAxis: true,
cursorOffsetX: 20,
cursorOffsetY: 20
},
// Scrollable configuration
scrollable: {
speed: 25,
sensitivity: 60,
scrollableElements: [document.querySelector('.scroll-container')]
},
// Announcement configuration
announcements: {
'drag:start': 'Picked up item',
'drag:stop': 'Dropped item',
expire: 5000
},
// Add additional plugins
plugins: [Plugins.Collidable, Plugins.SortAnimation],
// Plugin-specific options
collidables: '.obstacle',
sortAnimation: {
duration: 200,
easingFunction: 'cubic-bezier(0.4, 0.0, 0.2, 1)'
}
});Install with Tessl CLI
npx tessl i tessl/npm-shopify--draggable