How to integrate SortableJS with Alpine to create sortable lists (drag & drop)

Video thumbnail

When working with Alpine and you want to add advanced interaction without installing massive libraries, SortableJS is a lifesaver. I first used it in a to-do list app in Alpine JS, and since then it's become part of my toolbox for anything involving drag-and-drop reordering.

Below, I'll show you how to integrate it step-by-step using a CDN, just like we did earlier with data persistence in Alpine. I'll cover how to initialize it within Alpine and how to leverage it in a real-world use case.

What is SortableJS and why combine it with Alpine

SortableJS is a pure JavaScript library that allows you to turn any list into a sortable list by dragging elements.

The interesting thing is that it is not an Alpine-specific plugin—as I mentioned the first time I tried it—but that's not a problem: Alpine has enough hooks to integrate it perfectly.

Advantages of using it together with Alpine:

  • You don't need additional dependencies.
  • You can control the state from Alpine without rewriting anything.
  • The behavior is very stable even in long lists.
  • You can trigger events after moving an item to synchronize data.

The next operation we are going to perform is to allow the state to be sortable, that is, sortable via drag and drop. That we can select an item and place it elsewhere.

For this, it's very easy. A JavaScript plugin already exists—that is, it is not part of Alpine, but we can integrate it easily. The plugin is called SortableJS. We type "sortable js" (sortable) and enter the first link. As I mentioned, it is not a plugin specific to Alpine, but rather pure JavaScript.

Here you can see some demos of how it works: we click, hold it, and drag it. And, as I say, there are many possible implementations: for sharing, sorting, etc.

Installation with CDN

We are interested in installing it via the CDN. You can search for "sortable js cdn". If you don't find anything at first, you can go directly to the jsDelivr page or similar. In this case, we found the CDN, copy it, and paste it into our project.

I am going to place it right after the Alpine script, to maintain order. Its use is very simple, as you can see in the official documentation. Basically, it consists of getting the list of elements via a selector and then applying Sortable.create, passing the element to it. With that, the content would already be sortable. Quite simple.

<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script> 
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js" defer></script>

Integration with Alpine

Now, since this plugin is not part of the Alpine ecosystem, we have to figure out a way to integrate it. In Alpine, we have an attribute called x-init, which executes when the component is created. That is, it acts as a kind of constructor, and we can take advantage of it to do this initialization.

So, what we will do is reference our list—which is what we want to make sortable—using x-ref, although you can perfectly use a selector like those shown in the official documentation (for example, `document.querySelector`, `getElementById`, etc.).

In one of the examples, you can see that it is referenced directly like this. But since we like to complicate things, we are going to use x-ref.

Once the plugin is installed, we verify that we don't have 404 errors. Everything perfect. So now we can start.

I'm going to place an `x-init="order"` so that everything is organized. We are going to create this order method right below data. We place it like this:

order() {
  console.log("Inicializando sortable...");
}

Here you can see that, when the component is created, this method is called. Very simple. In this method, we take advantage of the initialization of Sortable.

Sortable.create(this.$refs.items);

Here we pass the ref that we defined as items. So, in our HTML we place:

<div x-data="{
        items: ['Uno', 'Dos', 'Tres'],
        order() {
            Sortable.create(this.$refs.items, {
                animation: 150
            });
        }
    }"
    x-init="order"
>
    <ul x-ref="items">
        <template x-for="item in items" :key="item">
            <li class="px-3 py-2 bg-gray-100 mb-1 cursor-move" x-text="item"></li>
        </template>
    </ul>
</div>

With this, everything should work correctly.

Useful Options

  • animation: smooths the movement.
  • handle: specific area where you can drag.
  • ghostClass: class applied to the "ghost" element while you drag.

Practical Case: Sortable To-Do List

I share a simplified version of the to-do list app I used in a course.
The trick lies in two things:

  • Integrating SortableJS inside Alpine.
  • Synchronizing the state after the user moves the items.
<div x-data="todoList()" x-init="init">
   <h2 class="font-bold text-xl mb-3">Mi To-Do List Ordenable</h2>
   <ul x-ref="tasks" class="space-y-2">
       <template x-for="(task, index) in tasks" :key="task.id">
           <li class="p-3 bg-blue-100 rounded cursor-move" x-text="task.label"></li>
       </template>
   </ul>
</div>
<script>
function todoList() {
   return {
       tasks: [
           { id: 1, label: 'Revisar documentación' },
           { id: 2, label: 'Crear ejemplo con Alpine' },
           { id: 3, label: 'Probar integración con SortableJS' },
       ],
       init() {
           Sortable.create(this.$refs.tasks, {
               animation: 150,
               onEnd: (evt) => {
                   // Reordenar array según nueva posición
                   const moved = this.tasks.splice(evt.oldIndex, 1)[0];
                   this.tasks.splice(evt.newIndex, 0, moved);
               }
           });
       }
   }
}
</script>

This ensures that the interface and the internal state remain synchronized.

It is the most "magical" part of the process, and one of the reasons why I love combining Alpine with external libraries.

Summary

To summarize a bit:

  • We use x-init to initialize our component, ideal for this type of external plugins like SortableJS.
  • We use x-ref as an alternative to `querySelector`, `getElementById`, etc.
  • We saw how to install and use a plugin that is not part of the Alpine ecosystem directly in our components.
  • With this, you can enrich your interface without the need for heavier tools.

Tips, Common Problems, and Best Practices

  • If SortableJS is not working, check that the Alpine component where you call it exists in the DOM.
  • Avoid using x-for with non-existent keys (always use `:key="id"`).
  • If you are going to handle thousands of elements, reduce the animation or disable it to save resources.
  • For large projects, create separate Alpine functions to initialize the lists.
  • If you have several connected lists, use the group option.

Conclusion

Integrating SortableJS with Alpine is surprisingly simple.
You only need an x-init, an x-ref, and a couple of initialization lines.
In my case, I first integrated it into a small to-do project and it worked so smoothly that I adopted it for panels, dashboards, and even dynamic forms.

If you want to create cleaner experiences without depending on heavy frameworks, this combination is one of the best out there.

Frequently Asked Questions (FAQs)

  • Can I use SortableJS without Alpine?
    • Yes, SortableJS works alone, but Alpine allows you to control the state more easily.
  • What happens if I use multiple components on the same page?
    • Nothing special, but you must make sure that each list has its own x-ref.
  • Where is the best place to initialize SortableJS within Alpine?
    • In x-init, because it acts as the component's constructor.
  • How do I update the state after moving an element?
    • Capture the onEnd event of SortableJS and reorder the internal array (as in the to-do list example).

I agree to receive announcements of interest about this Blog.

Learn how to integrate SortableJS with Alpine.js to create reorderable lists with drag and drop. This tutorial guides you step-by-step to add interactivity to your projects without heavyweight libraries, easily synchronizing state.

| 👤 Andrés Cruz

🇪🇸 En español