The JavaScript Drag & Drop library your grandparents warned you about.
—
Sortable extends Draggable to provide reordering functionality for elements within or between containers. It automatically tracks position changes and provides events for handling sort operations.
Creates a sortable instance with the same interface as Draggable but with additional sorting-specific functionality.
/**
* Creates a new sortable instance for reordering elements
* @param containers - Elements that contain sortable items
* @param options - Configuration options (same as Draggable)
*/
class Sortable<T = SortableEventNames> extends Draggable<T> {
constructor(containers: DraggableContainer, options?: DraggableOptions);
}Usage Example:
import { Sortable } from "@shopify/draggable";
const sortable = new Sortable(document.querySelectorAll('.sortable-list'), {
draggable: '.sortable-item'
});
// Listen for sort events
sortable.on('sortable:sorted', (event) => {
console.log(`Moved from index ${event.oldIndex} to ${event.newIndex}`);
console.log('Old container:', event.oldContainer);
console.log('New container:', event.newContainer);
});Methods for getting element positions within containers.
/**
* Returns the current index of an element within its container during drag
* @param element - Element to get index for
* @returns Zero-based index of the element
*/
index(element: HTMLElement): number;
/**
* Returns sortable elements for a specific container, excluding mirror and original source
* @param container - Container to get elements from
* @returns Array of sortable elements
*/
getSortableElementsForContainer(container: HTMLElement): HTMLElement[];Usage Example:
sortable.on('sortable:sort', (event) => {
const currentIndex = sortable.index(event.dragEvent.source);
const allItems = sortable.getSortableElementsForContainer(event.dragEvent.sourceContainer);
console.log(`Item ${currentIndex + 1} of ${allItems.length}`);
});Sortable-specific events that fire during sort operations.
type SortableEventNames =
| 'sortable:start'
| 'sortable:sort'
| 'sortable:sorted'
| 'sortable:stop'
| DraggableEventNames;Event Details:
Event Handlers Example:
sortable.on('sortable:start', (event) => {
console.log('Sort started');
console.log('Start index:', event.startIndex);
console.log('Start container:', event.startContainer);
});
sortable.on('sortable:sort', (event) => {
// This fires before the move happens - you can cancel it
if (shouldPreventSort(event.dragEvent.over)) {
event.cancel();
}
});
sortable.on('sortable:sorted', (event) => {
// This fires after the element has been moved
updateDataModel(event.oldIndex, event.newIndex, event.oldContainer, event.newContainer);
});
sortable.on('sortable:stop', (event) => {
console.log('Final position - Old:', event.oldIndex, 'New:', event.newIndex);
saveChangesToServer();
});class SortableEvent extends AbstractEvent {
readonly dragEvent: DragEvent;
}
class SortableStartEvent extends SortableEvent {
readonly startIndex: number;
readonly startContainer: HTMLElement;
}
class SortableSortEvent extends SortableEvent {
readonly oldIndex: number;
readonly newIndex: number;
readonly oldContainer: HTMLElement;
readonly newContainer: HTMLElement;
}
class SortableSortedEvent extends SortableEvent {
readonly oldIndex: number;
readonly newIndex: number;
readonly oldContainer: HTMLElement;
readonly newContainer: HTMLElement;
}
class SortableStopEvent extends SortableEvent {
readonly oldIndex: number;
readonly newIndex: number;
readonly oldContainer: HTMLElement;
readonly newContainer: HTMLElement;
}import { Sortable } from "@shopify/draggable";
// Create sortable for todo lists
const todoSortable = new Sortable(document.querySelectorAll('.todo-list'), {
draggable: '.todo-item',
handle: '.todo-handle',
classes: {
'source:dragging': 'todo-dragging',
'container:over': 'todo-drop-zone'
}
});
// Track changes for persistence
let pendingChanges = [];
todoSortable.on('sortable:sorted', (event) => {
const change = {
itemId: event.dragEvent.source.dataset.id,
oldIndex: event.oldIndex,
newIndex: event.newIndex,
oldListId: event.oldContainer.dataset.listId,
newListId: event.newContainer.dataset.listId
};
pendingChanges.push(change);
// Show visual feedback
event.dragEvent.source.classList.add('recently-moved');
setTimeout(() => {
event.dragEvent.source.classList.remove('recently-moved');
}, 1000);
});
todoSortable.on('sortable:stop', () => {
// Batch save all changes
if (pendingChanges.length > 0) {
saveTodoChanges(pendingChanges);
pendingChanges = [];
}
});
// Cleanup
function destroyTodoSortable() {
todoSortable.destroy();
}Sortable automatically handles sorting between different containers:
const kanbanSortable = new Sortable([
document.querySelector('.backlog'),
document.querySelector('.in-progress'),
document.querySelector('.done')
], {
draggable: '.kanban-card'
});
kanbanSortable.on('sortable:sorted', (event) => {
const card = event.dragEvent.source;
const newStatus = event.newContainer.dataset.status;
const oldStatus = event.oldContainer.dataset.status;
if (newStatus !== oldStatus) {
// Update card status when moved between columns
updateCardStatus(card.dataset.id, newStatus);
card.querySelector('.status').textContent = newStatus;
}
});Install with Tessl CLI
npx tessl i tessl/npm-shopify--draggable