JavaScript library for reorderable drag-and-drop lists on modern browsers and touch devices
—
Rich event lifecycle with callbacks for drag start/end, list changes, and custom interactions.
Events that track the complete drag and drop lifecycle from selection to completion.
Called when an element is selected for dragging.
/**
* Called when element is chosen (selected for dragging)
* @param evt - Event object with oldIndex property
*/
onChoose?: (evt: SortableEvent) => void;Usage Examples:
Sortable.create(el, {
onChoose: (evt) => {
console.log('Item chosen at index:', evt.oldIndex);
evt.item.classList.add('being-dragged');
}
});Called when element selection is cancelled before dragging starts.
/**
* Called when element selection is cancelled
* @param evt - Event object with same properties as onEnd
*/
onUnchoose?: (evt: SortableEvent) => void;Called when dragging starts (after any configured delay).
/**
* Called when dragging actually starts
* @param evt - Event object with oldIndex and item properties
*/
onStart?: (evt: SortableEvent) => void;Usage Examples:
Sortable.create(el, {
onStart: (evt) => {
console.log('Drag started:', evt.item.textContent);
document.body.classList.add('dragging-active');
}
});Called when dragging ends (whether successful or cancelled).
/**
* Called when dragging ends
* @param evt - Event object with comprehensive drag information
*/
onEnd?: (evt: SortableEvent) => void;Usage Examples:
Sortable.create(el, {
onEnd: (evt) => {
document.body.classList.remove('dragging-active');
if (evt.oldIndex !== evt.newIndex) {
console.log(`Item moved from ${evt.oldIndex} to ${evt.newIndex}`);
// Save new order
saveOrder(evt.from);
}
}
});Events that fire when the list structure changes due to drag operations.
Called when an element is added to the list from another list.
/**
* Called when element is added from another list
* @param evt - Event object with same properties as onEnd
*/
onAdd?: (evt: SortableEvent) => void;Usage Examples:
Sortable.create(targetList, {
onAdd: (evt) => {
console.log('Item added from another list:', evt.item.textContent);
console.log('Added at index:', evt.newIndex);
// Update data model
addItemToList(evt.item.dataset.id, evt.newIndex);
// Animate the new item
evt.item.classList.add('newly-added');
setTimeout(() => evt.item.classList.remove('newly-added'), 300);
}
});Called when element order changes within the same list.
/**
* Called when list order changes within the same list
* @param evt - Event object with same properties as onEnd
*/
onUpdate?: (evt: SortableEvent) => void;Called when an element is removed from the list to another list.
/**
* Called when element is removed to another list
* @param evt - Event object with same properties as onEnd
*/
onRemove?: (evt: SortableEvent) => void;Called for any list change (add, update, or remove).
/**
* Called for any change to the list (add/update/remove)
* @param evt - Event object with same properties as onEnd
*/
onSort?: (evt: SortableEvent) => void;Usage Examples:
Sortable.create(el, {
onSort: (evt) => {
// This fires for any list change
console.log('List changed:', evt.type);
// Save to localStorage
const order = Array.from(evt.to.children)
.map(item => item.dataset.id);
localStorage.setItem('list-order', JSON.stringify(order));
}
});Events for handling special interactions and custom drag behavior.
Called when attempting to drag a filtered (non-draggable) element.
/**
* Called when trying to drag a filtered element
* @param evt - Event object with item property
*/
onFilter?: (evt: SortableEvent) => void;Usage Examples:
Sortable.create(el, {
filter: '.locked',
onFilter: (evt) => {
console.log('Cannot drag locked item:', evt.item.textContent);
// Show user feedback
evt.item.classList.add('shake-animation');
setTimeout(() => evt.item.classList.remove('shake-animation'), 500);
}
});Called when an element moves during dragging, allows controlling insertion behavior.
/**
* Called when element moves during drag, can control insertion
* @param evt - Event object with movement data
* @param originalEvent - Original DOM event
* @returns Control insertion: false (cancel), -1 (before), 1 (after), true/void (default)
*/
onMove?: (evt: SortableEvent, originalEvent: Event) => boolean | number | void;Usage Examples:
Sortable.create(el, {
onMove: (evt, originalEvent) => {
// Prevent dropping on certain elements
if (evt.related.classList.contains('no-drop')) {
return false; // Cancel the move
}
// Custom insertion logic
if (evt.related.classList.contains('insert-before')) {
return -1; // Force insert before
}
if (evt.related.classList.contains('insert-after')) {
return 1; // Force insert after
}
// Allow default behavior
return true;
}
});Called when an element is cloned (in clone mode).
/**
* Called when element is cloned
* @param evt - Event object with item (original) and clone properties
*/
onClone?: (evt: SortableEvent) => void;Called when the dragging element changes position during drag.
/**
* Called when dragging element changes position
* @param evt - Event object with newIndex property
*/
onChange?: (evt: SortableEvent) => void;Usage Examples:
Sortable.create(el, {
onChange: (evt) => {
console.log('Element moved to index:', evt.newIndex);
// Real-time preview of changes
updatePreview(evt.newIndex);
}
});interface SortableEvent {
/** Target list element */
to: HTMLElement;
/** Source list element */
from: HTMLElement;
/** The dragged element */
item: HTMLElement;
/** Clone element (when using clone mode) */
clone?: HTMLElement;
/** Element's old index within parent */
oldIndex: number;
/** Element's new index within parent */
newIndex: number;
/** Element's old index within parent, only counting draggable elements */
oldDraggableIndex: number;
/** Element's new index within parent, only counting draggable elements */
newDraggableIndex: number;
/** Pull mode when item is in another sortable: "clone" if cloning, true if moving */
pullMode?: string | boolean;
/** Element on which have guided (onMove event only) */
related?: HTMLElement;
/** DOMRect of related element (onMove event only) */
relatedRect?: DOMRect;
/** DOMRect of dragged element (onMove event only) */
draggedRect?: DOMRect;
/** Whether Sortable will insert drag element after target by default (onMove event only) */
willInsertAfter?: boolean;
}Sortable.create(el, {
onChoose: (evt) => console.log('1. Item chosen'),
onStart: (evt) => console.log('2. Drag started'),
onChange: (evt) => console.log('3. Position changed'),
onEnd: (evt) => console.log('4. Drag ended'),
onAdd: (evt) => console.log('5a. Item added (if from another list)'),
onUpdate: (evt) => console.log('5b. Item updated (if same list)'),
onRemove: (evt) => console.log('5c. Item removed (if to another list)'),
onSort: (evt) => console.log('6. Any sort change occurred')
});Sortable.create(el, {
onEnd: (evt) => {
// Save to server
const newOrder = Array.from(evt.to.children)
.map(item => item.dataset.id);
fetch('/api/save-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order: newOrder })
});
}
});const lists = [el1, el2, el3].map(el =>
Sortable.create(el, {
group: 'shared',
onAdd: (evt) => {
console.log(`Item moved to list ${evt.to.id}`);
updateListCounters();
},
onRemove: (evt) => {
console.log(`Item left list ${evt.from.id}`);
updateListCounters();
}
})
);Install with Tessl CLI
npx tessl i tessl/npm-sortablejs