Alpine.js x-sort: Drag and Drop Sorting Without an External Library
AI generated
x-data
Alpine
Alpine.js · x-sort · Drag and Drop · Sortable Lists
Alpine.js x-sort
Drag and Drop Sorting Without an External Library

x-sort brings native drag and drop sorting to Alpine.js, without SortableJS, without jQuery UI, and without external dependencies. Sortable lists, moving items across groups, drag handles, and server synchronization can all be built with a handful of HTML attributes.

14 min read x-sort · Groups · Handles · Callbacks · Server Sync Alpine.js 3.x · Modern Browsers

1. What x-sort in Alpine.js Does

x-sort is an official Alpine.js plugin that wraps drag and drop sorting into a declarative directive. Internally, x-sort relies on the browser's native HTML Drag and Drop API, but it offers a much higher level of abstraction than working directly with dragstart, dragover, drop, and dragend events. Building a sortable list without x-sort usually means writing 50 to 100 lines of event handler code for a single list, with x-sort that shrinks down to two HTML attributes.

The difference compared to external libraries like SortableJS or jQuery UI Sortable is that x-sort brings no dependency of its own and is fully integrated into Alpine.js's reactivity system. That means Alpine state changes are immediately reactive after sorting: there is no manual reading of DOM state and no reconciliation between the DOM and the data model. x-sort taps directly into Alpine's reactivity system and keeps the data model automatically in sync with the visual order in the DOM.


// Install via npm
import Alpine from 'alpinejs';
import sort from '@alpinejs/sort';

Alpine.plugin(sort);
Alpine.start();

// Alternatively via CDN:
// <script src="https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.x.x/dist/cdn.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

// Basic structure: x-sort on the container, x-sort:item on the child elements
// <ul x-data x-sort>
//   <li x-sort:item>Element 1</li>
//   <li x-sort:item>Element 2</li>
// </ul>

A common misconception: x-sort is not a complete drag and drop library for arbitrary drag scenarios. It is specifically optimized for sorting lists. For drag and drop between entirely different components, for example dragging files onto an upload field, the native Drag and Drop API or a specialized library is a better fit. For the typical admin panel scenario (changing the order, moving items between columns), x-sort is the most direct and maintainable solution in the Alpine ecosystem.

2. Installing and Setting Up the x-sort Plugin

Like every Alpine.js plugin, x-sort has to be registered before Alpine starts. The critical point is that the order of script tags or imports matters. The plugin must be loaded before Alpine.start(), otherwise Alpine will not recognize the x-sort directive and will log an error to the console. With the CDN approach that means the plugin script tag has to come before the Alpine script tag.

For Magento 2 Hyva themes, the npm approach through the build process is recommended, since Hyva assembles the Alpine bundle via Tailwind CSS and its own build stack. The plugin is registered in the theme's Alpine initialization file that Hyva provides. That way x-sort stays part of the bundle instead of being loaded as a separate script, which keeps CSP compliance and performance intact.

3. A Simple Sortable List with x-sort

The simplest sortable list needs only two attributes: x-sort on the container and x-sort:item on every sortable child element. Alpine then automatically registers all the necessary event listeners and handles the visual feedback while dragging. No further JavaScript is needed, which is the core advantage of x-sort over a manual drag and drop implementation.

One important detail for the data model: by default x-sort only manipulates the DOM, not the Alpine data model. To keep the Alpine array in the correct order after sorting, you pass a callback to x-sort that writes the new order back into the Alpine data object. That is the only step that goes beyond plain HTML setup, and it takes just a few lines.


<!-- Simple sortable to-do list -->
<div x-data="{
  tasks: [
    { id: 1, title: 'Design-Review durchführen', done: false },
    { id: 2, title: 'Staging deployen', done: false },
    { id: 3, title: 'Tests schreiben', done: true },
    { id: 4, title: 'Code-Review anfordern', done: false },
  ],
  reorder(newOrder) {
    // newOrder is an array of { item, position } objects
    this.tasks = newOrder.map(({ item }) => this.tasks.find(t => t.id === parseInt(item)));
  }
}">
  <ul
    x-sort="reorder($item, $position)"
    class="space-y-2"
  >
    <template x-for="task in tasks" :key="task.id">
      <li
        x-sort:item.string="task.id"
        class="flex items-center gap-3 bg-white border border-slate-200 rounded-xl px-4 py-3 cursor-grab active:cursor-grabbing shadow-sm hover:shadow-md transition-shadow"
      >
        <input type="checkbox" :checked="task.done" class="rounded">
        <span :class="task.done ? 'line-through text-slate-400' : 'text-slate-800'" x-text="task.title"></span>
      </li>
    </template>
  </ul>
</div>

4. Drag Handles: Making Only Certain Areas Draggable

In most UIs the entire list item should not be draggable, only a dedicated handle, an icon or area that clearly signals to the user that dragging can start here. This prevents accidental drag actions when clicking buttons, links, or form fields inside the list item. Adding x-sort:handle to a child element configures exactly this behavior: only the handle element starts a drag, the rest of the item behaves normally.

The handle icon should be visually unambiguous. The classic six-dot drag icon ( or an SVG grid) is well established in admin interfaces. It also helps to use the CSS cursor grab on the handle and grabbing in the active state. The handle itself needs no JavaScript, it is a plain HTML attribute on the element. Important: the handle element should not have its own click handlers, since the browser treats drag actions and click events differently.


<!-- Sortable list with drag handle -->
<div x-data="{
  items: [
    { id: 1, name: 'Kategorie: Elektronik', count: 42 },
    { id: 2, name: 'Kategorie: Kleidung', count: 128 },
    { id: 3, name: 'Kategorie: Bücher', count: 67 },
    { id: 4, name: 'Kategorie: Sport', count: 35 },
  ],
  saveOrder(item, position) {
    // Remember ID and new position for server sync
    console.log('Verschoben:', item, '→ Position:', position);
  }
}">
  <ul x-sort="saveOrder($item, $position)" class="space-y-2">
    <template x-for="row in items" :key="row.id">
      <li
        x-sort:item="row.id"
        class="flex items-center gap-3 bg-white border border-slate-200 rounded-xl px-4 py-3 shadow-sm"
      >
        <!-- Only this element is the drag trigger -->
        <span
          x-sort:handle
          class="cursor-grab active:cursor-grabbing text-slate-300 hover:text-slate-500 transition-colors flex-shrink-0"
          title="Zum Sortieren ziehen"
        >
          <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
            <path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"/>
          </svg>
        </span>
        <span class="flex-1 font-medium text-slate-800" x-text="row.name"></span>
        <span class="text-xs text-slate-400 bg-slate-100 rounded px-2 py-0.5" x-text="row.count + ' Produkte'"></span>
      </li>
    </template>
  </ul>
</div>

5. Groups: Moving Items Between Lists

One of the most powerful features of x-sort is cross-group sorting: items can be moved between multiple container lists as long as both containers share the same group name. This is the classic Kanban board pattern, moving tasks between "To Do", "In Progress", and "Done". With x-sort and the .group modifier this is possible without any additional JavaScript code.

The callback passed to x-sort receives the new position and the target container when moving across groups. The Alpine data model then has to be updated accordingly: the item is removed from the source list and inserted into the target list. For simple Kanban boards, a central Alpine store that holds both lists and provides the callback handler is enough.


<!-- Kanban board: drag between three columns -->
<div x-data="{
  columns: {
    todo:  [{ id:1, text:'Feature A' }, { id:2, text:'Bug B' }],
    doing: [{ id:3, text:'Refactoring C' }],
    done:  [{ id:4, text:'Deploy D' }],
  },
  move(fromCol, toCol, itemId, newIndex) {
    const item = this.columns[fromCol].find(i => i.id === itemId);
    this.columns[fromCol] = this.columns[fromCol].filter(i => i.id !== itemId);
    this.columns[toCol].splice(newIndex, 0, item);
  }
}">
  <div class="grid grid-cols-3 gap-4">
    <template x-for="[colKey, colItems] in Object.entries(columns)" :key="colKey">
      <div class="bg-slate-50 rounded-xl p-4 min-h-48">
        <h3 class="font-bold text-sm uppercase tracking-wider text-slate-500 mb-3" x-text="colKey"></h3>
        <ul
          x-sort.group.kanban="(item, pos) => move(colKey, $el.dataset.col, parseInt(item), pos)"
          :data-col="colKey"
          class="space-y-2 min-h-8"
        >
          <template x-for="card in colItems" :key="card.id">
            <li
              x-sort:item="card.id"
              class="bg-white border border-slate-200 rounded-lg px-3 py-2 text-sm font-medium text-slate-700 cursor-grab shadow-sm"
              x-text="card.text"
            ></li>
          </template>
        </ul>
      </div>
    </template>
  </div>
</div>

6. Callbacks: Reacting to Sort Events

The callback passed to the x-sort directive is the central integration point between the visual drag and drop interaction and the Alpine data model. x-sort provides two magic variables: $item holds the value set via x-sort:item (typically an ID), and $position holds the new zero-based index position after the drop. These two values are all that is needed to update the data model and send a server request.

For more complex scenarios, for example when sorting needs to be approved by a validation step, or when an asynchronous server request fails and the previous order needs to be restored, an optimistic update pattern is recommended: Alpine stores the previous order before sorting, sends the request, and restores the previous order if it fails. This is the same pattern used for forms and mutation requests, and it fits naturally into Alpine's fetch integration.

7. Server Synchronization After Sorting

Client-side sorting alone is not enough for many admin panel scenarios: the new order has to be persisted on the server so it survives a reload. The pattern for this is straightforward: inside the x-sort callback, after updating the Alpine data model, a fetch request is sent that transmits the new order (as an array of IDs or as an ID-to-position mapping) to the server.

Error handling matters here: if the server request fails, the previous order has to be restored. The optimistic update pattern updates the order immediately in the frontend for instant user feedback, sends the request asynchronously, and resets the frontend to the previous state on failure. A small toast notification informs the user about the error without blocking the entire workflow.


<!-- Sortable list with server synchronization and optimistic update -->
<div x-data="{
  items: [], // Wird vom Server geladen
  saving: false,
  error: null,
  previousOrder: [],

  async init() {
    const r = await fetch('/api/categories/order');
    this.items = await r.json();
  },

  async reorder(itemId, newPosition) {
    // Optimistic: update Alpine state immediately
    this.previousOrder = [...this.items];
    const idx = this.items.findIndex(i => i.id === parseInt(itemId));
    const [moved] = this.items.splice(idx, 1);
    this.items.splice(newPosition, 0, moved);

    // Persist to server
    this.saving = true;
    this.error = null;
    try {
      const response = await fetch('/api/categories/order', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ order: this.items.map((item, i) => ({ id: item.id, position: i })) }),
      });
      if (!response.ok) throw new Error('Server-Fehler ' + response.status);
    } catch (e) {
      // Rollback on failure
      this.items = this.previousOrder;
      this.error = 'Reihenfolge konnte nicht gespeichert werden.';
    } finally {
      this.saving = false;
    }
  }
}">
  <div class="flex items-center gap-3 mb-4">
    <h2 class="font-bold text-slate-800">Kategorien sortieren</h2>
    <span x-show="saving" class="text-xs text-teal-600 animate-pulse">Speichern…</span>
    <span x-show="error" x-text="error" class="text-xs text-red-600"></span>
  </div>

  <ul x-sort="reorder($item, $position)" class="space-y-2">
    <template x-for="item in items" :key="item.id">
      <li x-sort:item="item.id"
          class="flex items-center gap-3 bg-white border border-slate-200 rounded-xl px-4 py-3 cursor-grab shadow-sm">
        <span x-sort:handle class="text-slate-300 hover:text-slate-500 cursor-grab">⠿</span>
        <span x-text="item.name" class="flex-1 font-medium text-slate-800"></span>
      </li>
    </template>
  </ul>
</div>

8. x-sort vs. Manual Drag and Drop Alternatives Compared

Deciding whether x-sort or an external library is the right choice comes down to a few concrete comparison points: bundle size, feature scope, integration with Alpine, and maintenance effort.

Criterion x-sort (Alpine Plugin) SortableJS Native HTML Drag & Drop
Bundle Size ~3 KB gzipped ~15 KB gzipped 0 KB (native)
Alpine Integration Native, reactive Manual DOM sync required Fully manual
Drag Handles x-sort:handle handle option Implement manually
Groups/Kanban .group modifier group option Very complex custom impl.
Touch Support Browser-dependent Built-in No native touch support
Maintenance Effort Minimal (HTML attributes) Moderate JS configuration High (many event handlers)

Touch support is the only significant advantage SortableJS has over x-sort: SortableJS ships its own touch event polyfill that works on mobile devices, while the native HTML Drag and Drop API is limited on touch devices. For admin panels used mainly on desktop, x-sort is the better choice. For mobile-first drag and drop scenarios, for example reordering images in a mobile theme editor, SortableJS or a specialized touch library should be evaluated.

Mironsoft

Alpine.js Admin Panel Components for Magento 2

Sortable Admin Panels and Kanban Boards for Magento 2?

We build sortable product, category, and content lists with x-sort for Magento 2, implement server synchronization, and integrate the components cleanly into the Hyva admin ecosystem.

Drag and Drop Lists

x-sort with handles, groups, and server sync for Magento admin panels

Kanban Boards

Cross-column moves with optimistic updates and rollback

Persistence

REST API or GraphQL integration for durable order storage

9. Summary

x-sort solves the drag and drop sorting problem in Alpine.js declaratively and without external dependencies. The combination of x-sort on the container, x-sort:item on the items, x-sort:handle for dedicated drag grips, and the .group modifier for cross-column moves covers all common sorting scenarios in admin panels. The callback parameter supplies $item and $position for syncing with the Alpine data model and the server.

The only significant caveat compared to SortableJS is touch support on mobile devices. For desktop-oriented admin panels, the typical Magento 2 backend or a Hyva-based CMS, x-sort is the cleaner, leaner, and more maintainable solution. Its direct integration into Alpine's reactivity system eliminates the manual DOM state reconciliation that is always a potential source of bugs with external libraries.

x-sort in Alpine.js: The Essentials at a Glance

Basic Setup

x-sort on the container plus x-sort:item on the child elements. Install the plugin via npm or CDN and register it before Alpine.start().

Drag Handles

x-sort:handle on a child element makes only that element the drag trigger. Prevents accidental drags when clicking buttons and links.

Groups / Kanban

x-sort.group.NAME on multiple containers allows cross-column moves. $item and $position in the callback for data model sync.

Server Sync

Optimistic update: update Alpine state immediately, send the fetch request, reset to previousOrder on failure. Toast notification for user feedback.

10. FAQ: Alpine.js x-sort

1What is x-sort in Alpine.js?
Official Alpine.js plugin for declarative drag and drop sorting. Uses the native HTML Drag and Drop API and is reactively integrated into Alpine, so no manual DOM reading is required.
2Do I need SortableJS for x-sort?
No. x-sort is standalone, with no dependency on SortableJS. Installation: npm install @alpinejs/sort or a CDN script tag placed before the Alpine script.
3How do I register x-sort?
Alpine.plugin(sort) called before Alpine.start(). With CDN: place the plugin script tag before the Alpine script tag.
4How does x-sort:item work?
Marks an element as sortable and assigns it a value (typically an ID). This value is available in the callback as $item.
5Building a drag handle with x-sort?
x-sort:handle on a child element of the item. Only that element then starts the drag. A grab/grabbing cursor and a drag icon are recommended.
6Kanban: moving items between lists?
All containers get the same .group.NAME modifier: x-sort.group.kanban. The callback updates the Alpine data model (removes from the source list, inserts into the target list).
7What are $item and $position?
$item is the value set via x-sort:item (typically an ID). $position is the zero-based index of the new position after the drop.
8Saving the new order on the server?
In the callback, update the Alpine state and send a fetch request. Optimistic update: show it immediately in the frontend, reset to previousOrder on failure.
9Touch support with x-sort?
Limited. The native HTML Drag and Drop API does not offer full touch support. For mobile sorting scenarios, evaluate SortableJS with its touch polyfill.
10Advantage of x-sort vs. manual drag events?
x-sort replaces 50-100 lines of event handler code with 2 HTML attributes. Reactive Alpine integration eliminates DOM state reconciliation. Groups and handles work without additional JS code.