Lightweight JavaScript Sortable Drag and Drop Library


Reorder lists, horizontal rows, grids, and kanban-style boards in plain HTML and JavaScript. hcg-sortable is a zero-dependency library built on the Pointer Events API, so mouse, touch, and pen input share one code path. It supports connected lists, drag handles, custom placeholders, FLIP sibling animation, and edge auto-scroll without jQuery or a framework.

On this page you will find live demos, installation (direct download, npm, CDN), a full options reference, React notes, and copy-paste examples for connected lists, handles, placeholders, and callbacks. Open the full interactive demo page for all nine try-it-yourself examples.

What is hcg-sortable?

hcg-sortable is a lightweight vanilla JavaScript library for making lists, rows, and grids reorderable. Call new HcgSortable() on a container, constrain sorting with axis modes and handles, move items between connected lists with group, and read the new order with toArray() - without React, jQuery, or any other dependency.

hcg-sortable examples showing vertical list sorting, horizontal row sorting, and grid sorting with drag handles and placeholders
hcg-sortable examples: vertical list, horizontal row, and grid sorting with drag handles and placeholders.

Why use hcg-sortable?

Most sortable solutions are either tied to a framework or ship as part of a large drag-and-drop toolkit when you only need to reorder DOM children. jQuery UI Sortable works, but it depends on jQuery and jQuery UI, and touch support needs extra plugins.

hcg-sortable fills that gap: a small, dependency-free library built on Pointer Events for mouse, touch, and pen. It suits vanilla pages, React or Vue dashboards, todo lists, kanban boards, and builder UIs without pulling in a heavy toolkit. See the comparison table for how it differs from jQuery UI Sortable and native HTML drag and drop, and Features for the full capability list.

Live Demo

Drag the items below. Each block shows a different option. For every demo in one place, see the full interactive demo page.

Vertical list (axis: y)

Default vertical sorting with placeholder and animation.

  • Item A
  • Item B
  • Item C
  • Item D

Horizontal row (axis: x)

Sort items left and right in a flex row.

One
Two
Three
Four

Grid (axis: grid)

Two-dimensional grid sorting.

1
2
3
4
5
6
7
8

Drag handle + filter

Only the handle starts a drag. The button is filtered and stays clickable.

  • Drag by handle
  • Handle only
  • Body won't drag

Connected lists (group)

Drag items between two lists that share a group name.

List A

  • A-1
  • A-2
  • A-3

List B

  • B-1
  • B-2

Comparison Table

How hcg-sortable compares with common alternatives.

Feature hcg-sortable jQuery UI Sortable Native HTML Drag and Drop
DependenciesNonejQuery + jQuery UINone
Touch and pen supportYes (Pointer Events)Needs extra pluginInconsistent
Sortable lists and gridsBuilt inLists built in, grids need extra workRequires custom logic
Connected listsBuilt in with groupBuilt in with connectWithRequires custom logic
Drag handlesYesYesManual
Custom placeholdersYesLimitedManual
FLIP sibling animationYesYesManual
Auto-scrollYesYesManual
Framework requirementNoneRequires jQueryNone

Installation

Direct download

Or download hcg-sortable.js and hcg-sortable.css and include them directly:

HTML
<link rel="stylesheet" href="hcg-sortable.css">
<script src="hcg-sortable.js"></script>
NPM

Install from npm:

Command line
npm install hcg-sortable

CommonJS (require):

JavaScript
require("hcg-sortable/hcg-sortable.css");
const HcgSortable = require("hcg-sortable");

ESM (import):

JavaScript
import HcgSortable from "hcg-sortable";
import "hcg-sortable/hcg-sortable.css";
CDN usage

Load from a CDN with two tags - no npm or bundler required. The global HcgSortable class is available as soon as the script finishes loading.

jsDelivr:

HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hcg-sortable@1/hcg-sortable.css">
<script src="https://cdn.jsdelivr.net/npm/hcg-sortable@1/hcg-sortable.js"></script>

unpkg:

HTML
<link rel="stylesheet" href="https://unpkg.com/hcg-sortable@1/hcg-sortable.css">
<script src="https://unpkg.com/hcg-sortable@1/hcg-sortable.js"></script>

Replace @1 or @1.0.0 with the release you want. Load CSS in <head> before the script. You can also self-host by uploading hcg-sortable.js and hcg-sortable.css to your own server.

Basic Usage

Create a container with child elements, then pass the container selector or element to new HcgSortable().

HTML
<ul id="todo-list" class="list">
  <li class="item" data-id="one">One</li>
  <li class="item" data-id="two">Two</li>
  <li class="item" data-id="three">Three</li>
</ul>
JavaScript
const sortable = new HcgSortable('#todo-list', {
    axis: 'y',
    animation: 150,
    onUpdate(detail) {
      console.log('Moved from', detail.oldIndex, 'to', detail.newIndex);
    }
  });

Try it live: vertical list


Horizontal row and grid

Set axis to 'x' for a horizontal row or 'grid' for a two-dimensional grid.

JavaScript
new HcgSortable('#row',  { axis: 'x',    animation: 150 });
new HcgSortable('#grid', { axis: 'grid', animation: 150 });

Try it live: horizontal row | grid


Connected lists

Lists that share a group name can exchange items. Use pull and put to control transfers.

JavaScript
var opts = {
  axis: 'y',
  animation: 150,
  group: { name: 'shared', pull: true, put: true }
};
new HcgSortable('#list-a', opts);
new HcgSortable('#list-b', opts);

Try it live: connected lists


Drag by a handle

Pass a handle selector so only that child element starts a drag. Use cancel to exclude buttons or inputs (same as jQuery UI Sortable).

JavaScript
new HcgSortable('#list', {
  handle: '.handle',
  cancel: '.no-drag'
});

Try it live: drag handle


zIndex
JavaScript
new HcgSortable('#list', {
  zIndex: 2000
});

Try it live: zIndex

Read the new order with toArray()

After a sort, call toArray() to get an array of each item's data-id value (or its index when no data-id is set).

JavaScript
const sortable = new HcgSortable('#tasks', {
  onUpdate() {
    console.log(sortable.toArray()); // e.g. ['b', 'a', 'c']
  }
});

Custom placeholder and animation

Pass HTML for a custom placeholder, set animation duration, and use className for the dragged item style.

JavaScript
new HcgSortable('#list', {
  placeholder: '<li class="ghost">drop here</li>',
  className: 'dragging-custom',
  animation: 250,
  dragThreshold: 8
});

Try it live: custom placeholder


Auto-scroll and containment

Enable edge auto-scroll inside scrollable containers and optionally constrain the dragged item.

JavaScript
new HcgSortable('#scroll-list', {
  axis: 'y',
  scroll: true,
  scrollSpeed: 14,
  scrollThreshold: 36,
  containment: document.getElementById('scroll-list')
});

Try it live: auto-scroll

Animation on and off
JavaScript
new HcgSortable('#list', {
  animation: true   // default 150 ms; false or 0 to disable
});

Try it live: animation


Cursor style

Set cursor on items when sorting is enabled. disable() restores the normal arrow cursor.

JavaScript
new HcgSortable('#list', {
  cursor: 'grab'   // 'move', 'pointer', etc.; false opts out
});

Try it live: cursor + disabled


Change options at runtime

Call setOption(name, value) or option(name, value) on an instance to change any option after creation.

JavaScript
var s = new HcgSortable('#list');
s.setOption('animation', 0);   // disable FLIP animation
s.setOption('dragThreshold', 20);
s.disable();                   // turn sorting off
s.enable();                    // turn sorting back on

Features

Everything included in the library at a glance:

  • Zero dependencies - one JS file and one CSS file.
  • Pointer Events - mouse, touch, and pen through one code path.
  • Axis modes - vertical list, horizontal row, 2D grid, or automatic layout detection.
  • Connected lists - move items between containers with group.
  • Drag handles - restrict the grab area with a selector.
  • Cancel regions - exclude buttons, links, or inputs from starting a drag (jQuery UI Sortable compatible).
  • zIndex - control floated item stacking while dragging.
  • Cursor - configurable item cursor; disabled lists use the default arrow.
  • Custom placeholders - generated gap, HTML string, class name, or disabled.
  • FLIP animation - smooth sibling reordering with configurable duration.
  • Auto-scroll - edge scrolling for scrollable containers or the page.
  • Containment - constrain the dragged item to a parent or element.
  • Drag threshold - require a small move before dragging begins.
  • Callbacks - onStart, onMove, onAdd, onRemove, onUpdate, and onEnd.
  • Instance API - enable, disable, option, setOption, toArray, and destroy.
  • UMD friendly - works from a script tag, CommonJS, AMD, or as a global HcgSortable class.

Using with React

Create the sortable inside a useEffect hook and clean it up with destroy() when the component unmounts.

JavaScript
import { useEffect, useRef } from "react";
import HcgSortable from "hcg-sortable";
import "hcg-sortable/hcg-sortable.css";

function SortableList() {
  const listRef = useRef(null);

  useEffect(() => {
    const sortable = new HcgSortable(listRef.current, {
      axis: "y",
      animation: 150,
      onUpdate(detail) {
        console.log(detail.oldIndex, detail.newIndex);
      }
    });
    return () => sortable.destroy();
  }, []);

  return (
    <ul ref={listRef}>
      <li data-id="one">First</li>
      <li data-id="two">Second</li>
      <li data-id="three">Third</li>
    </ul>
  );
}

If React state owns the list data, update that state inside onUpdate, onAdd, or onRemove so the rendered order stays in sync with the DOM.

Options Reference

All options

OptionTypeDefaultDescription
groupString, Object, or nullnullConnects lists for moving items between containers. Object form: { name, pull, put }.
handleString or nullnullCSS selector for a child element inside each item that starts the drag.
cancelString or nullnullCSS selector for child elements that must not start a drag (jQuery UI Sortable compatible).
axisString'y''x', 'y', 'grid', or 'auto'. Controls drop-slot detection direction.
classNameString'hcg-sortable-drag'Class added to the item while it is being dragged.
placeholderBoolean or StringtrueGenerated placeholder, HTML string, custom class name, or false to disable.
animationNumber150FLIP sibling animation duration in milliseconds. Use 0 to disable.
scrollBoolean or ElementtrueEnables edge auto-scroll for the nearest scrollable container, page, or a provided element.
containmentString, Element, Object, or nullnullConstrains the dragged item to a parent, element, or coordinate rectangle.
dragThresholdNumber4Pointer movement in pixels required before dragging begins.
disabledBooleanfalseDisables sorting when set to true.
cursorString or false'grab'Cursor on items when enabled. Use false to manage cursors in your own CSS. Disabled lists use default.
zIndexNumber1000Stacking order of the floated item while dragging.
onStartFunction or nullnullFires when a drag starts.
onMoveFunction or nullnullFires while the placeholder moves to a new slot.
onAddFunction or nullnullFires on the list that receives an item from another connected list.
onRemoveFunction or nullnullFires on the list that loses an item to another connected list.
onUpdateFunction or nullnullFires when item order or list placement changes.
onEndFunction or nullnullFires when the drag interaction ends.

Pass these as the second argument to new HcgSortable(element, options). After creation, read or change any option with option(name) (getter) or option(name, value) / setOption(name, value) (setter).

Getter and setter

Every option in the table below supports runtime get and set on the instance:

JavaScript
const sortable = new HcgSortable('#list', {
  axis: 'y',
  animation: 150
});

// Getter — one argument returns the current value
sortable.option('axis');       // 'y'
sortable.option('animation');  // 150
sortable.option('group');      // null, or the object/string you set

// Setter — two arguments update the option (returns the instance for chaining)
sortable.option('animation', 0);
sortable.setOption('dragThreshold', 20);  // jQuery UI Sortable-style alias

sortable
  .setOption('cursor', 'move')
  .setOption('zIndex', 2000);

sortable.setOption('group', {
  name: 'workflow',
  pull: true,
  put: false
});

// Prefer enable() / disable() for sorting on/off; option('disabled') also works
sortable.option('disabled', true);
sortable.enable();

option('filter', value) is accepted as a legacy alias for cancel. Setting animation normalizes true (default ms), false or 0 (off), or a positive number. Setting group, cursor, or disabled updates internal state immediately (connected-list rules, cursor CSS, etc.).

Instance Methods

The constructor returns a sortable instance with the following methods.

MethodDescription
toArray()Returns the current order as an array of data-id values (or indices when no data-id is set).
option(name)Gets an option value.
option(name, value)Sets an option value. Returns the instance for chaining.
setOption(name, value)Alias for option(name, value).
enable()Turns sorting on. Returns the instance.
disable()Turns sorting off without removing listeners. Returns the instance.
destroy()Removes all listeners and classes. Use this for SPA or framework cleanup.

Read the library version from the static property: console.log(HcgSortable.version);

Callbacks

Pass lifecycle hooks as option functions. Each callback receives one argument, detail, and this is the HcgSortable instance the callback belongs to (the list that registered the option).

Function signature
JavaScript
new HcgSortable('#tasks', {
  onStart(detail) { console.log('Started', detail.oldIndex); },
  onMove(detail)  { console.log('Moving to', detail.newIndex); },
  onAdd(detail)   { console.log('Added to', detail.to); },
  onRemove(detail){ console.log('Removed from', detail.from); },
  onUpdate(detail){ console.log(detail.oldIndex, '->', detail.newIndex); },
  onEnd(detail)   { console.log('Ended', detail.item); }
});
detail properties

All callbacks share the same detail shape. Not every property is meaningful on every callback (see table below).

PropertyTypeDescription
eventPointerEventThe pointer event that triggered this callback.
itemHTMLElementThe element being dragged.
fromHTMLElementThe source list container (where the drag started).
toHTMLElementThe destination list container (current target while dragging, final target on drop).
oldIndexnumberIndex of item among sortable children when the drag started (respects the items selector).
newIndexnumberOn onMove: index of the active drop slot while dragging. On drop callbacks: final index after the item is inserted.
When each callback runs
CallbackWhen it runsthis isdetail highlights
onStart Pointer moved past dragThreshold and dragging begins. Source list instance item, oldIndex, from, to (same as from at start)
onMove Drop slot or target list changes during the drag. Source list instance oldIndex, live newIndex, to updates when hovering another list
onAdd Item dropped into a different connected list. Destination list instance from and to differ; newIndex is index in destination
onRemove Item left the original list for another connected list. Source list instance from and to differ; oldIndex is where it started
onUpdate Order changed within a list, or item moved between lists. Fires on each affected list. List whose order changed oldIndex -> newIndex when position changed
onEnd Pointer released; drag finished (even if position did not change). Source list instance Final item, from, to, oldIndex, newIndex
Example

Log the full detail object while building a handler, then read the fields you need:

JavaScript
new HcgSortable('#tasks', {
  onStart(detail) {
    console.log(detail);
  },

  onMove(detail) {
    console.log('slot', detail.oldIndex, '->', detail.newIndex, 'in', detail.to.id);
  },

  onAdd(detail) {
    console.log('added', detail.item, 'to', detail.to, 'at', detail.newIndex);
  },

  onRemove(detail) {
    console.log('removed', detail.item, 'from', detail.from, 'was at', detail.oldIndex);
  },

  onUpdate(detail) {
    console.log('updated', detail.oldIndex, '->', detail.newIndex);
  },

  onEnd(detail) {
    console.log('ended', detail.item, detail.from.id, '->', detail.to.id);
  }
});

CSS Classes

hcg-sortable adds a few class names you can target for styling. The drag class can be changed with the className option.

ClassApplied to
hcg-sortableThe container element the instance is attached to.
hcg-sortable-dragThe item currently being dragged (configurable via className).
hcg-sortable_placeholderThe generated placeholder shown at the drop position.
hcg-sortable-draggingAdded to body during a drag to force a grabbing cursor.

Browser Support

hcg-sortable works in all modern browsers that support the Pointer Events API, CSS transforms, and requestAnimationFrame.

BrowserSupported
Google ChromeYes
Mozilla FirefoxYes
Microsoft EdgeYes
SafariYes
OperaYes
Mobile browsersYes (touch sorting supported)

License

Released under the MIT License. Free for personal and commercial use. Copyright HTML Code Generator.

Frequently Asked Questions (FAQ)

Does hcg-sortable have any dependencies?

No. hcg-sortable is plain vanilla JavaScript with zero dependencies. Include hcg-sortable.js and hcg-sortable.css and you are ready to go.

Does hcg-sortable work on touch devices?

Yes. It is built on the Pointer Events API, so mouse, touch, and pen input all work through one code path.

Can hcg-sortable move items between connected lists?

Yes. Lists can share a group name and use pull and put settings to allow or restrict transfers between containers.

Can I use hcg-sortable with React?

Yes. Create the HcgSortable instance inside a useEffect hook, keep a ref to the container element, and call destroy() in the cleanup function when the component unmounts. See the Using with React section on this page.

Can I load hcg-sortable from a CDN?

Yes. Add link and script tags for hcg-sortable.css and hcg-sortable.js from jsDelivr, unpkg, or your own hosted copy, then call new HcgSortable('.selector'). See Installation and CDN usage on this page.

Do connected lists need a minimum height?

Yes. When a connected list becomes empty, its container collapses to zero height and cannot receive drops. Set min-height on each list container with CSS, or set minHeight in JavaScript during drag when a list is empty.