
JSort is a JavaScript library that enables smooth, touch-enabled, drag-and-drop sorting for HTML lists and grids.
Features:
- Zero dependencies: Pure vanilla JS with no external libraries required.
- Touch and mouse support: Unified pointer events API handles all input methods.
- Smooth animations: All displaced elements animate naturally during reordering.
- Linked groups: Drag items between multiple containers sharing a group name.
- Swap mode: Exchange positions instead of reordering the entire list.
- Nested sortables: Parent and child containers can both be sortable.
- Scroll-intent detection: On touch devices, brief taps trigger clicks while drags initiate sorting.
- Action-element protection: Automatically ignores grabs on inputs, buttons, links, and contenteditable fields.
- Edge scrolling: Containers scroll automatically when dragging near boundaries.
- Delegated events: Works with dynamically added items without reinitialization.
- Custom Handles: You can restrict the drag action to a specific “handle” element.
Use Cases
- Dashboard widget rearrangement: Users reorder analytics cards across multiple dashboard sections with linked groups.
- Mobile-first task managers: Touch-optimized list reordering with scroll-intent prevents accidental drags during vertical scrolling.
- E-commerce product sorting: Admin interfaces let merchants reorder products within categories using drag handles.
- Nested content builders: Page builders with sortable rows containing sortable columns require nested sortable support.
- Team roster management: Swap-mode lets coaches exchange player positions between offense/defense groups without full reordering.
How To Use It:
1. Install the package via NPM.
npm install @rbuljan/jsort
2. Create your HTML structure. You can use any parent tag, such as a <ul> or a <div>.
<ul id="my-list">
<li>Item 1</li>
<li>Item 2</li>
<li>
<!-- Optional drag handle -->
<span class="handle">::</span>
Item 3
</li>
<li>Item 4</li>
</ul>3. Initialize the library in your JavaScript file.
import JSort from '@rbuljan/jsort';
// Initialize the sortable list
// The first argument is the container element
// The second argument is the configuration object
const sortable = new JSort(document.getElementById("my-list"), {
duration: 300,
selectorHandler: '.handle', // Optional: restricts drag to the handle
onDrop: (data) => {
// Log the data to see the new index
console.log('Item dropped:', data);
}
});4. Available configuration options. You can pass these options to the second argument of the JSort constructor:
group(string): Links multiple sortable containers. Items can be dragged between containers with matching group names. Default is empty string (no linking).swap(boolean): Changes drop behavior to swap positions instead of reordering. Useful for ranked lists or team rosters. Default is false.duration(number): Animation duration in milliseconds for the sort animation. Default is 420.easing(string): CSS easing function for animations. Default is “cubic-bezier(0.6, 0, 0.6, 1)”.scale(number): Scale factor for the ghost element that follows the pointer. Default is 1.1.opacity(number): Opacity value for the ghost element, ranging from 0 to 1. Default is 0.8.grabTimeout(number): Delay in milliseconds before grab activates on touch devices. This allows scroll-intent detection. Default is 140. Has no effect on mouse events.parentDrop(boolean): Allows dropping items directly onto the parent container instead of between items. Default is true.moveThreshold(number): Distance in pixels the pointer must move before drag activates. Prevents accidental drags when clicking links or buttons. Default is 0.scrollThreshold(number): Distance in pixels to consider touch movement as scrolling instead of dragging. Default is 8.edgeThreshold(number): Distance in pixels from scrollable container edge to trigger auto-scroll. Default is 50.scrollSpeed(number): Scroll velocity in pixels per animation frame during edge auto-scroll. Default is 10.zIndex(number): Z-index value for the ghost element. Default is 2147483647 (maximum 32-bit signed integer).selectorParent(string): CSS selector for parent sortable containers. Default is “.jsort”.selectorItems(string): CSS selector for sortable items within the parent. Uses immediate children by default. Default is “*”.selectorItemsIgnore(string): CSS selector for ignored children that should not be sortable. Default is “.jsort-ignore”.selectorHandler(string): CSS selector for drag handle elements within items. Default is “.jsort-handler”.selectorIgnoreTarget(string): CSS selector for item descendants that should prevent grab when clicked. Default is empty string.selectorIgnoreFields(string): CSS selector for action elements that should prevent grab. Default targets inputs, buttons, links, and other interactive elements.classGhost(string): Class name applied to the ghost element. Default is “is-jsort-ghost”.classActive(string): Class name applied on pointer down. Default is “is-jsort-active”.classTouch(string): Class name applied only on touch events. Default is “is-jsort-touch”.classGrab(string): Class name applied to the grabbed item. Default is “is-jsort-grab”.classTarget(string): Class name applied to the hovered target element. Default is “is-jsort-target”.classAnimated(string): Class name applied to all animated elements during drop. Default is “is-jsort-animated”.classAnimatedDrop(string): Class name applied to the grabbed item during drop animation. Default is “is-jsort-animated-drop”.classInvalid(string): Class name applied to ghost element over invalid drop zones. Default is “is-jsort-invalid”.onBeforeGrab(function): Callback invoked before grab activates. Return false to cancel the grab operation.onGrab(function): Callback invoked when item grab activates.onMove(function): Callback invoked during pointer movement while dragging.onBeforeDrop(function): Callback invoked before drop completes. Return false to cancel the drop operation.onDrop(function): Callback invoked after successful drop.onAnimationEnd(function): Callback invoked when drop animation completes.
const sortable: new JSort(document.getElementById("my-list"), {
elGrabParent: el,
group: "",
swap: false,
duration: 420,
easing: "cubic-bezier(0.6, 0, 0.6, 1)",
scale: 1.1,
opacity: 0.8,
grabTimeout: 140,
parentDrop: true,
moveThreshold: 0,
scrollThreshold: 8,
edgeThreshold: 50,
scrollSpeed: 10,
zIndex: 2147483647 // 0x7FFFFFFF,
selectorParent: ".jsort",
selectorItems: "*",
selectorItemsIgnore: ".jsort-ignore",
selectorHandler: ".jsort-handler",
selectorIgnoreTarget: "",
selectorIgnoreFields: `:is(input, select, textarea, button, label, [contenteditable=""], [contenteditable="true"], [tabindex]:not([tabindex^="-"]), a[href]:not(a[href=""]), area[href]):not(:disabled)`,
classGhost: "is-jsort-ghost",
classActive: "is-jsort-active",
classTouch: "is-jsort-touch",
classGrab: "is-jsort-grab",
classTarget: "is-jsort-target",
classAnimated: "is-jsort-animated",
classAnimatedDrop: "is-jsort-animated-drop",
classInvalid: "is-jsort-invalid",
onBeforeGrab: () => { },
onGrab: () => { },
onMove: () => { },
onBeforeDrop: () => { },
onDrop: () => { },
onAnimationEnd: () => { },
}),5. Or define them directly in HTML using the data-jsort attribute as follows:
The
data-jsortformat uses semicolons to separate options. Use colons to separate option names from values. The library automatically parses numbers and booleans from string values.
<div id="playerRoster" data-jsort="
group: team-a;
selectorItems: .player;
selectorHandler: .handle;
swap: true;
duration: 300;
easing: ease-out;
zIndex: 999;
parentDrop: false;
">
<div class="player">
<div class="handle">⋮⋮</div>
<span>Player One</span>
</div>
<div class="player">
<div class="handle">⋮⋮</div>
<span>Player Two</span>
</div>
</div>new JSort(document.querySelector("#playerRoster"));6. JSort provides methods to control the sortable instance programmatically:
// Re-initialize with new options
sortableInstance.init({
duration: 500,
swap: true
});
// Clean up and remove event listeners
sortableInstance.destroy();
// Programmatically insert an item
const newItem = document.createElement('div');
newItem.textContent = 'New Task';
sortableInstance.insert(newItem, targetElement);
// Sort items with animation (Beta feature)
sortableInstance.sort((a, b) => {
// Sort alphabetically by text content
return a.textContent.localeCompare(b.textContent);
});7. Event handlers.
new JSort(document.querySelector("#myList"), {
// Called before grab activates (return false to cancel)
onBeforeGrab(data) {
console.log('Before grab:', data.elGrab, data.indexGrab);
console.log('Event:', data.event);
// Access instance properties via 'this'
console.log('Parent element:', this.elGrabParent);
},
// Called when grab activates
onGrab(data) {
console.log('Grabbed element:', data.elGrab);
console.log('From parent:', data.elGrabParent);
console.log('Original index:', data.indexGrab);
},
// Called during pointer movement
onMove(data) {
console.log('Moving over:', data.elTarget);
console.log('Valid target:', data.isValidTarget);
console.log('Ghost element:', data.elGhost);
},
// Called before drop completes (return false to cancel)
onBeforeDrop(data) {
console.log('Attempting drop at index:', data.indexDrop);
console.log('Is valid:', data.isValidTarget);
console.log('Same parent:', data.isSameParent);
},
// Called after successful drop
onDrop(data) {
console.log('Moved from', data.indexGrab, 'to', data.indexDrop);
console.log('Dropped element:', data.elDrop);
console.log('New parent:', data.elDropParent);
console.log('Affected elements:', this.affectedElements);
},
// Called when animation completes
onAnimationEnd() {
console.log('Animation finished');
// All affected elements have reached final positions
}
});8. Access instance properties to read the current drag state or affected elements.
const sortable = new JSort(document.querySelector("#list"));
// During drag operations:
console.log(sortable.indexGrab); // Index of grabbed item (-1 when not dragging)
console.log(sortable.indexDrop); // Target drop index (-1 when not dragging)
console.log(sortable.elGrab); // Grabbed element reference
console.log(sortable.elGrabParent); // Grabbed item's parent container
console.log(sortable.elGhost); // Ghost element that follows pointer
console.log(sortable.elTarget); // Currently hovered target
console.log(sortable.elDrop); // Final drop target element
console.log(sortable.elDropParent); // Drop target's parent container
console.log(sortable.affectedElements); // Array of elements moved by the drop9. JSort works without CSS but benefits from minimal styling for visual feedback. Add these styles to improve the user experience.
/* Highlight active item on touch devices */
.is-jsort-active.is-jsort-touch {
outline: 2px solid currentColor;
}
/* Hide the original grabbed element */
.is-jsort-grab {
opacity: 0;
}
/* Highlight valid drop targets */
.is-jsort-target {
z-index: 1;
outline: 2px dashed currentColor;
}
/* Show invalid drop zones */
.is-jsort-invalid {
outline: 2px solid red;
}Alternatives
- Sortable: The most popular drag-and-drop library with multi-drag and plugin system.
- Dragula: Simple drag-and-drop library with minimal configuration.
- Shopify Draggable: A modular drag-and-drop library.
FAQs:
Q: How do I prevent users from dragging specific items?
A: Add the selectorItemsIgnore class to items you want to exclude. Configure this through the options object or use a custom class name. You can also use the onBeforeGrab callback to conditionally prevent grabs based on element properties or state.
Q: Can I use JSort with dynamically added items?
A: Yes. JSort uses event delegation on the parent container. Items added to the DOM after initialization work automatically without re-initialization.
Q: How do I persist the new order after users reorder items?
A: Use the onDrop callback to capture the new order. Query all items and extract their IDs or data attributes, then send this data to your server.
Q: Why isn’t my list scrolling when I drag to the edge?
A: The parent container must have a defined height and overflow: auto or overflow: scroll in your CSS. JSort detects the scrollable parent automatically.







