Touch-Friendly JavaScript Drag and Drop Sortable Library – JSort

Category: Drag & Drop , Javascript | January 27, 2026
Authorrokobuljan
Last UpdateJanuary 27, 2026
LicenseMIT
Views26 views
Touch-Friendly JavaScript Drag and Drop Sortable Library – JSort

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-jsort format 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 drop

9. 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.

You Might Be Interested In:


Leave a Reply