htmx 4.0 is under construction — migration guide

Drag to Reorder

Change order of records with drag and drop

Basic usage

This pattern integrates the Sortable.js library with htmx to persist drag-and-drop reordering on the server.

On the client, wrap your items in a form that posts on the Sortable end event.

<form class="sortable" hx-post="/items" hx-trigger="end"> <div class="htmx-indicator">Updating...</div> <div><input type="hidden" name="item" value="1"/>Item 1</div> <div><input type="hidden" name="item" value="2"/>Item 2</div> <div><input type="hidden" name="item" value="3"/>Item 3</div> </form>
  • hx-post sends the new order to /items.
  • hx-trigger="end" fires when Sortable.js finishes a drag (the end event bubbles up to the form).
  • Each item has a hidden input so the server receives the ids in their new order.

On the server, read the item parameter (which arrives as an ordered list) and respond with the updated list HTML.

Notes

Sortable.js initialization

Initialize Sortable on your containers using the htmx:load event so it works after htmx swaps in new content.

document.addEventListener("htmx:load", (e) => { const sortables = e.detail.elt.querySelectorAll(".sortable"); for (const sortable of sortables) { const sortableInstance = new Sortable(sortable, { animation: 150, ghostClass: "sortable-ghost", // Make the `.htmx-indicator` unsortable filter: ".htmx-indicator", onMove: (evt) => evt.related.className.indexOf("htmx-indicator") === -1, // Disable sorting until the server responds onEnd: function (evt) { this.option("disabled", true); } }); // Re-enable sorting after the swap completes sortable.addEventListener("htmx:afterSwap", () => { sortableInstance.option("disabled", false); }); } });

Note that onEnd uses a regular function (not an arrow function) because it needs this to refer to the Sortable instance. Sorting is disabled during the request and re-enabled on htmx:afterSwap to prevent the user from reordering while the server is processing.