# Drag to Reorder
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<div id="demo-content" class="not-prose demo-container flex justify-center"></div>
## Basic usage
This pattern integrates the [Sortable.js](https://sortablejs.github.io/Sortable/) 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.
```html
<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`](https://four.htmx.org/reference/attributes/hx-post) sends the new order to `/items`.
- [`hx-trigger`](https://four.htmx.org/reference/attributes/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.
```js
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.
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.