Sortable

<s-sortable> makes its children draggable. It is a custom element wrapping SortableJS, so a list drags as soon as the element connects — no init call, no DOMContentLoaded handler. It moves the same DOM nodes the rest of the page reads: after a drop the new order is already in the DOM, and anything that reads the list sees it.

Source: SSortable.js

Gather the requirements
Design the schema
Build the endpoint
Ship it
<s-sortable>
    <div>Gather the requirements</div>
    <div>Design the schema</div>
    <div>Build the endpoint</div>
    <div>Ship it</div>
</s-sortable>

Every direct child is draggable. Reordering animates; animation sets the duration in milliseconds.

handle names a selector — only that part starts the drag, the rest of the item stays clickable.

<s-sortable handle="[grip]">
    <a href="/jira"><span grip></span> Jira</a>
    <a href="/confluence"><span grip></span> Confluence</a>
</s-sortable>

Between lists

Lists that share a group name exchange items. Drag from one list, drop into the other — SortableJS moves the node, and the node lands in the other list's DOM.

Water the plants
Fix the faucet
Paint the fence
Walk the dog
Buy groceries
<div>
    <s-sortable group="chores">
        <div>Water the plants</div>
        <div>Fix the faucet</div>
        <div>Paint the fence</div>
    </s-sortable>
    <s-sortable group="chores">
        <div>Walk the dog</div>
        <div>Buy groceries</div>
    </s-sortable>
</div>

When several independent widgets with the same group sit on one page — two kanban boards, say — boundary keeps them apart. It names a selector; an item moves only between lists under the same closest boundary ancestor. The Kanban example uses boundary="[kanban-space]" for exactly this.

Remembering the order

store persists the order across visits. The tag names a localStorage key, and each child carries its name in sort-item. When a drag ends, the element saves the sort-item names in DOM order under the key. On connect it puts the saved order back by itself — also while the page is still parsing, so there is no flicker and no load handler:

Alpha
Beta
Gamma
Delta
<s-sortable store="saradom-sortable-demo">
    <div sort-item="alpha">Alpha</div>
    <div sort-item="beta">Beta</div>
    <div sort-item="gamma">Gamma</div>
    <div sort-item="delta">Delta</div>
</s-sortable>

Reorder the list, reload the page: the order holds. The list may also arrive later, inside an Hx fragment — custom elements upgrade on insertion, so dragging and restoring work in a fragment without any wiring.

On the element's own tag the attributes are bare: group, handle, store. On other elements they carry the module prefix: sort-item. The tag itself is the scope.

Reacting to a drop

onsort runs after a drop, like a native event attribute: this is the element, event is the SortableJS end event, with oldIndex and newIndex on it. The new order is already in the DOM by then, so the handler reads the list — for example to POST it with Hx:

<s-sortable onsort="hx(this)" hx-post="/reorder"
            hx-vals-body="() => ({ order: this.all('[sort-item]').map((el) => el.attr('sort-item')) })">

Mechanism

connectedCallback creates the SortableJS instance on the element; disconnectedCallback destroys it. An element removed and re-inserted — a fragment swap, a modal — wires itself again on its own. When the global Sortable is missing, the element does nothing, so the page degrades to a static list instead of throwing.

Support

The element needs SortableJS loaded first — one <script> tag before the lists, or before SSortable.js itself. Everything else is standard custom elements, in every evergreen browser.

Next: Switch →