Sortable

A handle drags its row

Put sortable on the element that holds the rows. A .sortable-handle inside a row is the only place that drags it, and it is a button, so the arrow keys move its row too.

Color

Size

Material

Price

A page loads SortableJS itself, before the Tsunami script

The drag is SortableJS 1.15.7 (MIT licence). It is not in tsunami.min.js: a page that has a .sortable loads dist/sortable.min.js with its own <script> first. A page with no sortable list does not load it and is not affected. With the script missing, each .sortable logs an error and stays a plain list.

<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tsunami.min.js"></script>

A list with no handle is dragged by its rows

Without a handle there are no keys: the rows move by pointer only.

  • Home

  • Shop

  • Support

On a tbody, the rows of a table are reordered

Label Source
Color Product attribute
Availability Standard
Price Standard

data-sortable-ignore on a control keeps a press on it from starting a drag

In a list with no handle, a button or a link would otherwise start a drag. Its own click still happens.

  • Visa

  • PayPal

  • Klarna

data-disabled keeps the rows where they are

The rows stay put by pointer and by key, and the handles are dimmed.

Standard

Express

data-variant="overlay" lays the handle over a picture: it shows under the pointer

The arrow keys move a picture along the grid, left and right as well as up and down.

data-options takes any SortableJS option as JSON: here a 600ms animation

The options are laid over the component's own (animation 150, forceFallback true). Under prefers-reduced-motion the default animation is 0, so set it here only when you mean it.

Monday

Tuesday

Wednesday

The tsunami:reorder event gives the row and its old and new place, and toArray() the new order

Email

Phone

Address

Move a row.

The page reverses the order with toArray() and sort(), and turns sorting off with disabled

First

Second

Third

A list started by mount() takes callbacks: onMove keeps the pinned row first, onEnd reports the drop

Pinned, always first

Orders

Customers

Products

Drag a row.

sortable is the SortableJS instance: here the page changes one of its options

Draft

Review

Published

A page tells the save bar about a new order by writing it to a hidden input

A move sends no input event, because the order is not a form value. The page keeps the order in a hidden field and calls tsunami.saveBar.check(form); the bar then shows until the form is saved.

First

Second

Third

A page saves the order itself

The component sends nothing to a server. A page posts the order from its tsunami:reorder listener.

list.addEventListener("tsunami:reorder", (event) => {
  fetch("/filters/order/", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ order: event.target.toArray() }),
  });
});

API

The functions live on window.tsunami.sortable, and the list element carries the same members.

Function / member Description
tsunami.sortable.mount(el, options?) Starts SortableJS on a list, with or without the class, and starts it again if it already has one. options are SortableJS options, laid over data-options, and can hold functions. Throws when SortableJS is not loaded.
tsunami.sortable.get(el) The SortableJS instance, or null.
tsunami.sortable.destroy(el) Stops the list and gives the element back. Removing the list from the page does this itself.
tsunami.sortable.toArray(el) / el.toArray() Returns every row's data-id, in the current order.
tsunami.sortable.sort(el, order) / el.sort(order) Puts the rows in the order of a list of data-id values. It sends no tsunami:reorder event.
tsunami.sortable.disabled(el, value?) / el.disabled Reads and sets data-disabled.
el.sortable The SortableJS instance, for anything the component does not cover (read only).

Events

Event On Detail Description
tsunami:reorder The list; it bubbles { item, id, oldIndex, newIndex } Sent after a row is moved, by pointer or by key. item is the row, id its data-id, and the indexes count rows from 0. It is not sent for sort() or for a row moved to another list (SortableJS sends its own add and remove).

Keyboard

Key Description
Arrow Up, Arrow Left On a handle, moves its row up (or back) one place.
Arrow Down, Arrow Right On a handle, moves its row down (or forward) one place.
Tab Moves to the next handle. A handle is a button, so this is the browser's own.

A list with no .sortable-handle is dragged by its rows, and has no keys.

A move made with the keys is spoken through a live region: the handle's aria-label, then moved to position N of M.

SortableJS sends its own events on the same element: start, end, update and the rest. The component adds only tsunami:reorder.

Under prefers-reduced-motion the animation is 0. The drag uses SortableJS's fallback mode (a copy of the row follows the pointer), which also works on touch screens; the handle has touch-action: none.

The order is not a form value, so a move sends no input event. To mark a form changed, write the order to a hidden input and call tsunami.saveBar.check(form), as in the example above.

SortableJS 1.15.7 is MIT licensed (vendor/sortable/LICENSE.txt). It is a separate file, dist/sortable.min.js, not part of tsunami.min.js.

Attributes

Attribute / element Values Description
.sortable On the element that holds the rows: a ul, a tbody, an item group or a grid. Its children are the rows. A list inside a row is its own sortable.
data-id text On a row. Names the row: toArray() returns it and the tsunami:reorder event carries it.
.sortable-handle A button inside a row. With one in the list, only the handle drags, and the arrow keys move the row. The default icon is icon-grip-vertical.
aria-label text On a .sortable-handle. Names the handle for a screen reader, for example Reorder Color. A move made with the keys is spoken with it.
data-variant overlay On a .sortable-handle. Lays the handle over a picture in a position: relative row; it shows under the pointer and on focus.
data-sortable-ignore present On a control inside a row. A press on it never starts a drag. Its own click still happens.
data-disabled present On .sortable. Keeps the rows where they are, by pointer and by key, and dims the handles. Adding or removing it later is followed.
data-options JSON On .sortable. Any SortableJS option. It is laid over the component's own: animation 150 (0 under reduced motion), forceFallback true, filter [data-sortable-ignore], handle .sortable-handle when the list has one.
.sortable-ghost, .sortable-fallback, .sortable-chosen Added by SortableJS while a row is dragged: the gap where it will land, the copy under the pointer and the row itself.
.sortable-status Added by the script on first key move: a visually hidden live region that speaks the move.