@get-set/gs-sortable
v1.1.1
Published
Get-Set Sortable
Downloads
706
Maintainers
Readme
GSSortable
A dependency-free, drag-and-drop sortable plugin available in two flavours from one codebase:
- Native / vanilla JS — a
window.GSSortable(element, params)factory. - React — a
<GSSortable />component.
Both share the exact same engine (actions/, constants/, helpers/, types/), so behaviour is identical across the two. CSS is injected automatically in React (no stylesheet import needed) and shipped as a plain stylesheet for native use.
Features
- Three layouts:
column(vertical list),row(horizontal strip),grid(N-column grid viacount) - Smooth pointer-driven dragging with animated sibling reflow and drop-settle
- Cross-list drag — let one instance accept items dropped from named other instances (
acceptFrom) - Drag handles — use your own (
handlerselector) or auto-render a styled one (handleStyle:bar/dots/grip/lines) - Placeholder / ghost variants while dragging (
ghostStyle:solid/dashed/outline/glow) - Optional lift effect (drop-shadow on the grabbed item) and drop-zone highlight on the active container
- Colour
theme(light/dark/auto) and opt-in polishedcardStyle - Tunable reflow
animationduration +easingcurve, all reduced-motion aware - Drag a clone instead of the original (
takeClone), constrain to bounds (allowOutOfBox) - Responsive per-breakpoint overrides (
responsive) - Auto re-layout on item resize (built-in
ResizeObserver) and on scroll afterSortcallback with the live item list; imperativecalculate/refresh/destroy- React-only
gsxprop for scoped inline (CSS-in-JS) styles
Compatibility
| Target | Requirement |
|---|---|
| React (the <GSSortable> component) | React 16.8+ (Hooks are required), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. |
| Native / vanilla (window.GSSortable) | No framework. Any modern evergreen browser. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import server-side (no DOM access at module load). The sortable itself is browser-only, so render it inside a Client Component ('use client'). |
The peer range is
^16.8.0 || ^17.0 || ^18.0 || ^19.0, withreact/react-dommarked optional so the native build has zero framework dependencies.
Installation
npm i @get-set/gs-sortableThe package exposes both builds via conditional exports:
| Condition | Entry |
|---|---|
| import (React / bundlers) | dist/components/GSSortable.js (types: dist/components/GSSortable.d.ts) |
| require (native bundle) | dist-js/bundle.js |
Project layout
GSSortable.ts # native entry (webpack -> dist-js/bundle.js, window global)
components/GSSortable.tsx # React component (tsc -> dist/, npm entry)
actions/ # shared engine (init, calculate, drag, sort-end, motion, …)
constants/ # shared constants + default params
helpers/uihelpers.ts # shared helpers (GUID, CSS inject/remove, scss->css)
types/ # Params / Ref / Window augmentation
components/styles/ # SCSS + compiled CSS + CSS-as-TS (runtime injection for React)
styles/ # SCSS + compiled CSS (for <link> use by the native build)Build
npm install
npm run build # builds both targets
npm run build:js # native bundle -> dist-js/bundle.js
npm run build:react # React + types -> dist/Tests
Unit tests use Vitest + jsdom:
npm test # run once
npm run test:watchUsage — Native JS
Include the stylesheet and the bundle, then construct the sortable over a container whose direct children are the sortable items:
<link rel="stylesheet" href="styles/GSSortable.css" />
<div class="list">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
<script src="dist-js/bundle.js"></script>
<script>
new GSSortable(document.querySelector('.list'), {
type: 'column',
gap: '10px',
afterSort: (items) => console.log('sorted', items),
});
</script>window.GSSortable is a constructor-style factory: new GSSortable(element, params). The first argument is a real HTMLElement (not a selector). Each instance is registered under its reference key (auto-generated if omitted).
The native bundle also ships two convenience adapters (injected as a banner into dist-js/bundle.js):
HTMLElement.prototype adapter — call .GSSortable(params) directly on any element:
document.querySelector('.list').GSSortable({
type: 'column',
gap: '10px',
});jQuery adapter — registered as $.fn.GSSortable when jQuery is present on the page; it constructs one instance per matched element:
$('.list').GSSortable({
type: 'grid',
count: 3,
gap: '12px',
});All three forms (new GSSortable(el, params), el.GSSortable(params), $(sel).GSSortable(params)) create the same instance and register it in window.GSSortableConfigue.
Instance registry & methods
Look up a registered instance by its reference key via the global registry, then call its methods:
new GSSortable(document.querySelector('.list'), { reference: 'tasks' });
const instance = window.GSSortableConfigue.instance('tasks');
instance.calculate(); // re-measure + re-lay-out the items
instance.refresh(); // tear down + re-initialise (re-binds drag handlers)
instance.destroy(); // remove all GSSortable styling/behaviourwindow.GSSortableConfigue is the shared registry:
| Member | Description |
|---|---|
| references | Array of { key, ref } for every live instance. |
| instance(key) | Returns the Ref for a given reference key (or undefined). |
Usage — React
CSS is injected automatically when the component mounts — no stylesheet import required. Each direct child becomes a sortable item.
import GSSortable from '@get-set/gs-sortable';
export default function Tasks() {
return (
<GSSortable type="column" gap="10px" afterSort={(items) => console.log(items)}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</GSSortable>
);
}Next.js (App Router)
The sortable is browser-only, so use it from a Client Component:
'use client';
import GSSortable from '@get-set/gs-sortable';
export default function Tasks() {
return (
<GSSortable type="grid" count={3} cardStyle theme="auto" handleStyle="grip">
<div>One</div>
<div>Two</div>
<div>Three</div>
</GSSortable>
);
}Cross-container drag (acceptFrom)
Give each list a reference, then let a target list accept items dragged out of named sources:
<GSSortable reference="list-a" type="column">
<div>A1</div>
<div>A2</div>
</GSSortable>
<GSSortable reference="list-b" type="column" acceptFrom={['list-a']}>
<div>B1</div>
<div>B2</div>
</GSSortable>acceptFrom lists the reference keys of other instances whose items may be dropped into this one.
The gsx prop (React only)
gsx is a scoped CSS-in-JS map injected for this instance only (matched via a data-key attribute). It supports nested selectors:
<GSSortable
type="grid"
count={2}
gsx={{
backgroundColor: '#0b1220',
padding: '12px',
':hover': { outline: '1px solid #4f7cff' },
}}
>
<div>A</div>
<div>B</div>
</GSSortable>Ref / imperative API
Attach a ref to call the instance methods imperatively. The ref is typed as GSSortableHandle:
import { useRef } from 'react';
import GSSortable, { GSSortableHandle } from '@get-set/gs-sortable';
function Demo() {
const sortable = useRef<GSSortableHandle>(null);
return (
<>
<button onClick={() => sortable.current?.calculate()}>Re-layout</button>
<button onClick={() => sortable.current?.refresh()}>Refresh</button>
<button onClick={() => sortable.current?.destroy()}>Destroy</button>
<GSSortable ref={sortable} type="grid" count={3}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</GSSortable>
</>
);
}| Method | Signature | Description |
|---|---|---|
| calculate | () => void | Re-measure and re-lay-out the items (e.g. after the container resizes). |
| refresh | () => void | Tear down and re-initialise the instance (re-binds drag handlers, re-lays-out). |
| destroy | () => void | Remove all GSSortable styling/behaviour from the grid. |
All methods are no-ops until the instance has mounted. (In React, item changes are handled automatically — driving items via children re-inits on add/remove.)
Options
Every option below is also a React prop (plus the React-only gsx and children).
| Option | Type | Default | Description |
|---|---|---|---|
| reference | string | auto GUID | Unique key. Used by the registry and by acceptFrom. Auto-generated if omitted. |
| type | 'column' \| 'row' \| 'grid' | 'column' | Layout direction. See the catalog below. |
| count | number | 3 | Number of columns (grid type only). |
| gap | string | '' | CSS gap between items, e.g. '10px' or '10px 20px' (row col). |
| handler | string | '' | CSS selector for a drag handle inside each item. When set, only the handle starts a drag (and auto handleStyle rendering is skipped). |
| cancel | string | undefined | CSS selector; a pointer-down on a matching element (or its descendants) does not start a drag — so buttons/inputs/links stay interactive inside a whole-item (or handle) draggable. Works for mouse + touch. Example: 'button, input, textarea, select, a, [contenteditable], [data-no-drag]'. |
| width | string | 'auto' | Item width override hint. (Effective item width is normally computed from the layout; column/grid size items to fit the container.) |
| className | string | '' | Extra class added to the container element (React). |
| takeClone | boolean | false | Drag a clone; the original stays in place (no placeholder left behind). |
| allowOutOfBox | boolean | true | Allow dragging the item beyond the container bounds. false constrains the drag. |
| acceptFrom | string[] | [] | reference keys of other instances whose items may be dropped into this one. |
| responsive | ResponsiveOption[] | [] | Per-breakpoint param overrides — { windowSize, params }[] (sorted automatically). |
| animation | number | 400 | Sibling-reflow / drop-settle duration (ms). Respects reduced-motion (collapses to 0). |
| easing | string | cubic-bezier(0.22, 1, 0.36, 1) | CSS easing curve for reflow / drop-settle transitions. |
| theme | 'light' \| 'dark' \| 'auto' | 'light' | Colour theme for the container + default card UI. |
| cardStyle | boolean | false | Apply the polished default item-card styling (padding, radius, surface, subtle border + shadow). |
| handleStyle | 'none' \| 'bar' \| 'dots' \| 'grip' \| 'lines' | 'none' | Auto-render a styled drag handle on each item when no handler selector is set. none keeps whole-item dragging. |
| ghostStyle | 'solid' \| 'dashed' \| 'outline' \| 'glow' | 'solid' | Visual variant of the placeholder / ghost left behind while dragging. |
| lift | boolean | true | Lift the grabbed item with a drop-shadow + slight scale/tilt for tactile feedback. Respects reduced-motion. |
| dropZoneHighlight | boolean | true | Highlight the active drop-zone container while an item hovers over it. |
| dragClass | string | '' | Extra class added to the grabbed (in-move) element while dragging. |
| ghostClass | string | '' | Extra class added to the placeholder / ghost element. |
| beforeInit | () => void | — | Called at the start of each layout calculation (before items are positioned). |
| afterInit | () => void | — | Reserved lifecycle hook (declared on the params type). |
| afterSort | (items: NodeListOf<Element>) => void | () => {} | Called after a sort completes, with the container's current child item list. |
| gsx | NestedCSS | — | React only — scoped inline (CSS-in-JS) styles for this instance; supports nested selectors. |
Variant catalogs
type — layout
| Value | Description |
|---|---|
| column | Vertical list. Items span the container width and stack top-to-bottom. |
| row | Horizontal strip. Items keep their natural width and flow left-to-right; container height matches the tallest item. |
| grid | N-column grid. Items are sized to (containerWidth − gaps) / count; rows wrap every count items. |
handleStyle — auto-rendered drag handle
Only applied when no custom handler selector is set. A .gs-sortable-handle span is injected as the first child of each item.
| Value | Description |
|---|---|
| none | No handle injected — the whole item is draggable (default / legacy behaviour). |
| bar | A single horizontal bar / pill grip. |
| dots | A dotted (grid-of-dots) grip. |
| grip | A classic two-column dotted drag-grip. |
| lines | Stacked horizontal lines (hamburger-style). |
ghostStyle — placeholder / drop-ghost
The placeholder is the gap left behind (or shown at the target slot) while dragging.
| Value | Description |
|---|---|
| solid | Filled placeholder surface (default). |
| dashed | Dashed border outline. |
| outline | Thin solid outline, transparent fill. |
| glow | Accent-coloured glow / highlighted drop target. |
theme — colour theme
| Value | Description |
|---|---|
| light | Light surfaces and borders (default). |
| dark | Dark surfaces and borders. |
| auto | Follows the OS prefers-color-scheme setting. |
Examples
Native — grid with handles, dark theme, cross-list drop
<link rel="stylesheet" href="styles/GSSortable.css" />
<div id="backlog"><div>Task A</div><div>Task B</div></div>
<div id="done"></div>
<script src="dist-js/bundle.js"></script>
<script>
new GSSortable(document.getElementById('backlog'), {
reference: 'backlog',
type: 'grid',
count: 3,
gap: '12px',
theme: 'dark',
cardStyle: true,
handleStyle: 'grip',
ghostStyle: 'glow',
});
new GSSortable(document.getElementById('done'), {
reference: 'done',
type: 'grid',
count: 3,
gap: '12px',
acceptFrom: ['backlog'],
afterSort: (items) => console.log('done now has', items.length),
});
// Imperative control later:
window.GSSortableConfigue.instance('backlog').refresh();
</script>React — column list with a custom handle and lift
'use client';
import GSSortable from '@get-set/gs-sortable';
export default function Playlist() {
return (
<GSSortable
type="column"
gap="8px"
handler=".drag"
lift
dropZoneHighlight
animation={300}
easing="ease-out"
afterSort={(items) => console.log('order changed:', items.length)}
>
<div className="track"><span className="drag">≡</span> Song 1</div>
<div className="track"><span className="drag">≡</span> Song 2</div>
<div className="track"><span className="drag">≡</span> Song 3</div>
</GSSortable>
);
}License
ISC.
