The JavaScript Drag & Drop library your grandparents warned you about.
—
Droppable extends Draggable to create designated drop zones where draggable elements can be placed. It provides visual feedback for valid drop targets and handles drop validation.
Creates a droppable instance that requires dropzone configuration to define valid drop targets.
/**
* Creates a new droppable instance with designated drop zones
* @param containers - Elements that contain draggable items
* @param options - Configuration options including required dropzone
*/
class Droppable<T = DroppableEventNames> extends Draggable<T> {
constructor(containers: DraggableContainer, options: DroppableOptions);
}
interface DroppableOptions extends DraggableOptions {
dropzone: string | NodeList | HTMLElement[] | (() => NodeList | HTMLElement[]);
classes?: {[key in DroppableClassNames]: string};
}
type DroppableClassNames =
| DraggableClassNames
| 'droppable:active'
| 'droppable:occupied';Usage Example:
import { Droppable } from "@shopify/draggable";
const droppable = new Droppable(document.querySelectorAll('.drag-container'), {
draggable: '.draggable-item',
dropzone: '.drop-zone',
classes: {
'droppable:active': 'drop-zone-active',
'droppable:occupied': 'drop-zone-occupied'
}
});
// Listen for drop events
droppable.on('droppable:dropped', (event) => {
console.log('Item dropped into:', event.dropzone);
console.log('Dragged item:', event.dragEvent.source);
});Specialized class name management for dropzone-specific states.
/**
* Returns CSS class name for droppable-specific class identifiers
* @param name - Droppable class identifier
* @returns CSS class name string
*/
getClassNameFor(name: DroppableClassNames): string;Droppable-specific events that fire during drop operations.
type DroppableEventNames =
| 'droppable:start'
| 'droppable:dropped'
| 'droppable:returned'
| 'droppable:stop'
| DraggableEventNames;Event Details:
Event Handlers Example:
droppable.on('droppable:start', (event) => {
console.log('Started dragging from dropzone:', event.dropzone);
// Highlight valid drop zones
document.querySelectorAll('.drop-zone').forEach(zone => {
zone.classList.add('available-target');
});
});
droppable.on('droppable:dropped', (event) => {
console.log(`Item moved to new dropzone`);
handleItemDrop(event.dragEvent.source, event.dropzone);
// Optionally prevent the drop
// event.cancel();
});
droppable.on('droppable:returned', (event) => {
console.log('Item returned to original position');
handleItemReturn(event.dragEvent.source, event.dropzone);
});
droppable.on('droppable:stop', (event) => {
// Clean up highlighting
document.querySelectorAll('.drop-zone').forEach(zone => {
zone.classList.remove('available-target');
});
});class DroppableEvent extends AbstractEvent {
readonly dragEvent: DragEvent;
}
class DroppableStartEvent extends DroppableEvent {
dropzone: HTMLElement;
}
class DroppableDroppedEvent extends DroppableEvent {
dropzone: HTMLElement;
}
class DroppableReturnedEvent extends DroppableEvent {
dropzone: HTMLElement;
}
class DroppableStopEvent extends DroppableEvent {
dropzone: HTMLElement;
}The dropzone option can be specified in multiple ways:
// CSS selector string
const droppable1 = new Droppable(containers, {
draggable: '.item',
dropzone: '.drop-target'
});
// NodeList or HTMLElement array
const dropzones = document.querySelectorAll('.zone');
const droppable2 = new Droppable(containers, {
draggable: '.item',
dropzone: dropzones
});
// Function returning NodeList or HTMLElement array
const droppable3 = new Droppable(containers, {
draggable: '.item',
dropzone: () => document.querySelectorAll('.dynamic-zone')
});import { Droppable } from "@shopify/draggable";
// Create a file upload interface with drag and drop
const fileDroppable = new Droppable(document.querySelector('.file-area'), {
draggable: '.file-item',
dropzone: '.upload-zone, .trash-zone',
classes: {
'droppable:active': 'zone-highlight',
'droppable:occupied': 'zone-has-file',
'source:dragging': 'file-being-dragged'
}
});
// Handle different drop zones
fileDroppable.on('droppable:dropped', (event) => {
const file = event.dragEvent.source;
const zone = event.dropzone;
if (zone.classList.contains('upload-zone')) {
// Start upload process
uploadFile(file.dataset.fileId);
showUploadProgress(file);
} else if (zone.classList.contains('trash-zone')) {
// Delete file
deleteFile(file.dataset.fileId);
file.remove();
}
});
// Provide visual feedback
fileDroppable.on('droppable:start', (event) => {
// Show drop zones when dragging starts
document.querySelectorAll('.drop-zone').forEach(zone => {
zone.style.opacity = '1';
zone.style.transform = 'scale(1.05)';
});
});
fileDroppable.on('droppable:stop', (event) => {
// Hide drop zones when dragging ends
document.querySelectorAll('.drop-zone').forEach(zone => {
zone.style.opacity = '';
zone.style.transform = '';
});
});
// Handle validation
fileDroppable.on('droppable:dropped', (event) => {
const file = event.dragEvent.source;
const zone = event.dropzone;
// Check if file type is allowed in this zone
const allowedTypes = zone.dataset.allowedTypes?.split(',') || [];
const fileType = file.dataset.type;
if (allowedTypes.length > 0 && !allowedTypes.includes(fileType)) {
event.cancel();
showError(`${fileType} files not allowed in this zone`);
}
});const conditionalDroppable = new Droppable(containers, {
draggable: '.task',
dropzone: () => {
// Only return available drop zones based on current state
return Array.from(document.querySelectorAll('.task-column'))
.filter(column => !column.classList.contains('locked'));
}
});const nestedDroppable = new Droppable(document.querySelector('.workspace'), {
draggable: '.widget',
dropzone: '.panel, .sidebar, .main-area'
});
// Handle different drop contexts
nestedDroppable.on('droppable:dropped', (event) => {
const widget = event.dragEvent.source;
const target = event.dropzone;
// Configure widget based on drop target
if (target.classList.contains('sidebar')) {
widget.classList.add('sidebar-widget');
resizeWidget(widget, 'narrow');
} else if (target.classList.contains('main-area')) {
widget.classList.add('main-widget');
resizeWidget(widget, 'wide');
}
});Install with Tessl CLI
npx tessl i tessl/npm-shopify--draggable