mrkeyoor.com_
Sat 08 Aug 20:58 UTC
npmWeb Frontendupdated 08 Aug 2026

sortablejs

SortableJS turns an existing DOM container into a reorderable drag-and-drop list. It handles mouse and touch input, animation, drag handles, connected lists, cloning, auto-scrolling, filtering, and optional MultiDrag and Swap behavior without requiring a framework or jQuery. It changes the DOM while a drag happens, then reports the result through events such as onEnd, so application code remains responsible for updating and saving the real data model.

Verdict

SortableJS remains a capable framework-neutral choice for pointer and touch sorting, especially across connected lists. Do not mistake its smooth DOM behavior for a complete accessible or state-managed interaction; those parts remain application work.

API stability4/5The public surface is still the long-running 1.x constructor, options object, event callbacks, toArray, sort, option, save, and destroy methods documented in the README. Version 1.15.7 stays within that major, but plugin entry points and the distinction among core, default, and complete builds are extra compatibility surfaces that consumers must pin and test.
Docs4/5The README documents nearly every option, event field, instance method, plugin family, persistence hook, and build entry with runnable examples, and the linked demo site returns 200. It is still one very long reference, several linked articles date from 2018 or 2019, and accessibility and framework state synchronization do not receive a complete production guide.
Maintenance4/5Version 1.15.7 was published in February 2026 and the repository was pushed in March 2026, so this is active software rather than an abandoned 1.x package. The counter also shows 524 open issues and pull requests, which indicates a heavy support and triage burden and makes it harder to know when a reported browser edge case will receive attention.
Ecosystem5/5The package recorded 4,144,533 downloads last week and the repository has 31,165 stars. The README links maintained or historical integrations for React, Vue, Angular, Knockout, Ember, jQuery, and Meteor, plus CDN distribution and external TypeScript declarations, making SortableJS one of the easiest drag engines to encounter in an existing frontend stack.

Use it if

  • You need reorderable lists in plain JavaScript or across several frontend frameworks and want one DOM-level API
  • You need items to move or clone between connected lists with configurable pull and put rules
  • You need touch support, auto-scroll, drag handles, animated movement, or nested list behavior without building gesture handling yourself
  • You are maintaining an existing SortableJS 1.x integration and value its long-lived API and broad adapter ecosystem
Skip it if

Setup reality

Installation is one npm package, with no runtime dependencies, native build, credentials, or configuration file. The first surprise is choosing an entry point. import Sortable from 'sortablejs' is the default build and includes AutoScroll and OnSpill. The core ESM build leaves default plugins out, while the complete ESM build also includes MultiDrag and Swap. Named plugin imports must be mounted once with Sortable.mount before an instance uses their options. TypeScript users need @types/sortablejs separately because the npm package has no declarations. SortableJS owns DOM order during a drag, so React, Vue, and other declarative apps must copy evt.oldIndex and evt.newIndex into state or use the framework wrapper linked by the README; otherwise the next render can undo the move. Every sortable item should have a stable data-id if you use toArray, sort, or the store adapter. Empty drop targets need height or padding because emptyInsertThreshold only helps near an actual target box. Touch screens often need delayOnTouchOnly plus a touchStartThreshold, while clickable rows benefit from a handle or fallbackTolerance. There is no persistence by default, and destroy must run when a view unmounts to remove listeners and instance state.

Patterns

Make a list sortablecreate-sortable-list

import Sortable from 'sortablejs';

const list = document.querySelector('#tasks');
const sortable = Sortable.create(list, {
  animation: 150,
  ghostClass: 'task-ghost',
});

SortableJS changes the DOM immediately; this alone does not update an application state array or save the new order.

Apply a completed move to application statesync-array-order

const sortable = Sortable.create(list, {
  animation: 150,
  onEnd({ oldIndex, newIndex }) {
    if (oldIndex == null || newIndex == null || oldIndex === newIndex) return;
    const [moved] = tasks.splice(oldIndex, 1);
    tasks.splice(newIndex, 0, moved);
    saveTasks(tasks);
  },
});

Indexes count all children unless draggable narrows the set; use oldDraggableIndex and newDraggableIndex when non-draggable children are present.

Keep row text selectable with a handleuse-drag-handle

Sortable.create(list, {
  handle: '.drag-handle',
  draggable: '.task',
  animation: 150,
});

// Each .task contains a button or span with class .drag-handle

The README explains that sortable rows otherwise disable text selection; a handle confines drag initiation to a deliberate target.

Move items between connected listsconnect-two-lists

for (const list of document.querySelectorAll('[data-board-column]')) {
  Sortable.create(list, {
    group: 'tasks',
    animation: 150,
    onAdd(event) {
      persistMove(event.item.dataset.id, event.to.dataset.boardColumn, event.newIndex);
    },
  });
}

Both lists need the same group name. onAdd runs on the receiving list; use onRemove on the source if that side also needs bookkeeping.

Clone palette items into a canvasclone-between-lists

Sortable.create(palette, {
  group: { name: 'blocks', pull: 'clone', put: false },
  sort: false,
});

Sortable.create(canvas, {
  group: { name: 'blocks', pull: true, put: true },
  animation: 150,
});

pull: 'clone' copies the DOM element instead of moving it; create a new application-level ID in onAdd so cloned records do not share identity.

Save and restore order with the store adapterpersist-local-order

Sortable.create(list, {
  group: 'saved-tasks',
  dataIdAttr: 'data-id',
  store: {
    get(sortable) {
      return JSON.parse(localStorage.getItem(sortable.options.group.name) || '[]');
    },
    set(sortable) {
      localStorage.setItem(sortable.options.group.name, JSON.stringify(sortable.toArray()));
    },
  },
});

get runs once during initialization and set runs when a drop ends. Every draggable child needs a unique data-id value.

Apply a stored order programmaticallyrestore-server-order

const sortable = Sortable.create(list, {
  dataIdAttr: 'data-id',
  animation: 150,
});

const ids = await fetch('/api/task-order').then((r) => r.json());
sortable.sort(ids, true);

sort expects string IDs matching data-id attributes. IDs absent from the DOM cannot create items, and unlisted elements remain in the list.

Exclude buttons and inputs from draggingfilter-row-controls

Sortable.create(list, {
  filter: 'button, a, input, textarea, [data-no-drag]',
  preventOnFilter: false,
  onFilter(event) {
    if (event.target.matches('[data-delete]')) removeTask(event.item.dataset.id);
  },
});

preventOnFilter defaults to true and calls preventDefault, which can suppress the normal click or focus behavior of controls unless you change it.

Reject an invalid move in onMoverestrict-drop-target

Sortable.create(list, {
  onMove(event) {
    const locked = event.related.matches('[data-locked]');
    if (locked) return false;
    return event.willInsertAfter ? 1 : -1;
  },
});

Returning false cancels that insertion; -1 forces before and 1 forces after. Leaving the function without a return keeps the default decision.

Reduce accidental drags on touch screenstune-touch-drag

Sortable.create(list, {
  delay: 180,
  delayOnTouchOnly: true,
  touchStartThreshold: 4,
  fallbackTolerance: 4,
  animation: 150,
});

The README suggests a touchStartThreshold of 3 to 5 for sensitive screens; test real devices because delayed native drag behavior varies by browser.

Enable the MultiDrag pluginmount-multidrag

import Sortable, { MultiDrag } from 'sortablejs';

Sortable.mount(new MultiDrag());

Sortable.create(list, {
  multiDrag: true,
  selectedClass: 'is-selected',
  multiDragKey: 'CTRL',
});

Mount a plugin once before creating instances. MultiDrag is an extra plugin, not one of the default AutoScroll and OnSpill plugins.

Temporarily disable and then clean updisable-and-destroy

const sortable = Sortable.create(list);

sortable.option('disabled', true);
sortable.option('disabled', false);

// When the view is removed:
sortable.destroy();

disabled preserves the instance for later use; destroy removes sortable behavior completely and is the right cleanup when a component unmounts.

Alternatives

PackageRegistryPick it when
@dnd-kit/corenpmReact applications that need composable sensors and a stronger foundation for keyboard-accessible interactions
dragulanpmSmall framework-free projects that want a narrower container-to-container drag API with fewer sorting controls
@shopify/draggablenpmProjects that want separate draggable, sortable, swappable, and droppable modules instead of one list-focused API