Open source March 2026

Drag and drop sorting for Livewire, powered by SortableJS. It integrates through plain HTML attributes: no PHP package, no config, no service provider.

Livewire ships no drag and drop of its own. The official livewire-sortable relies on Shopify Draggable, which is heavier, less actively maintained, and has known problems with wire:navigate and the Livewire 3 lifecycle.

wire-sortable livewire-sortable
Engine SortableJS (actively maintained) Shopify Draggable
Multi-container wire:sortable.group + .container Separate API
Custom options wire:sortable.options Limited
Swap mode Built in No
Bundle size ~36 KB min ~60 KB min
wire:navigate Yes Partial

Requirements: Livewire 3+, and a JavaScript bundler (Vite, Webpack, esbuild).

Installation

npm install wire-sortable
// resources/js/app.js
import 'wire-sortable';

Without a build step, there is a CDN build:

<script src="https://unpkg.com/wire-sortable@latest/dist/wire-sortable.min.js"></script>

Simple list

class TodoList extends Component
{
    public $todos;

    public function mount()
    {
        $this->todos = Todo::orderBy('order')->get();
    }

    public function updateOrder($items)
    {
        foreach ($items as $item) {
            Todo::find($item['value'])->update(['order' => $item['order']]);
        }
    }
}
<ul wire:sortable="updateOrder">
    @foreach($todos as $todo)
        <li wire:sortable.item="{{ $todo->id }}" wire:key="todo-{{ $todo->id }}">
            {{ $todo->title }}
        </li>
    @endforeach
</ul>

Drag handles

With wire:sortable.handle, only the handle starts a drag:

<li wire:sortable.item="{{ $item->id }}" wire:key="item-{{ $item->id }}"
    class="flex items-center gap-3 p-3 bg-white rounded-lg shadow">
    <span wire:sortable.handle class="cursor-grab text-gray-400 hover:text-gray-600">
        <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">...</svg>
    </span>
    <span>{{ $item->name }}</span>
</li>

Kanban board

Items move between containers that share a group:

public function updateTaskOrder($items, $toStatus, $fromStatus)
{
    foreach ($items as $item) {
        Task::find($item['value'])->update([
            'order'  => $item['order'],
            'status' => $toStatus,
        ]);
    }

    $this->tasks = Task::orderBy('order')->get()->groupBy('status');
}
<div wire:sortable="updateTaskOrder"
     wire:sortable.group="board"
     wire:sortable.container="{{ $status }}"
     class="space-y-2 min-h-[100px]">

    @foreach($tasks[$status] ?? [] as $task)
        <div wire:sortable.item="{{ $task->id }}" wire:key="task-{{ $task->id }}"
             class="p-3 bg-white rounded shadow">
            {{ $task->title }}
        </div>
    @endforeach
</div>

Directives

Directive Description
wire:sortable="method" Livewire method called on sort. Receives ($items, $toContainer, $fromContainer)
wire:sortable.item="id" Marks an element as sortable. The value is passed in $items
wire:sortable.handle Only this element starts a drag (optional)
wire:sortable.group="name" Items move only between containers sharing a group
wire:sortable.container="id" Identifies the container, passed as $toContainer / $fromContainer
wire:sortable.options='JSON' Custom SortableJS options as JSON

The $items array

public function updateOrder($items, $toContainer = null, $fromContainer = null)
{
    // $items = [
    //     ['order' => 1, 'value' => '42'],
    //     ['order' => 2, 'value' => '17'],
    //     ['order' => 3, 'value' => '8'],
    // ]
}

Options

<div wire:sortable="updateOrder"
     wire:sortable.options='{
         "animation": 300,
         "delay": 100,
         "delayOnTouchOnly": true,
         "ghostClass": "opacity-50",
         "chosenClass": "ring-2 ring-blue-500",
         "dragClass": "shadow-2xl"
     }'>
Option Default
animation 150
ghostClass sortable-ghost
chosenClass sortable-chosen

Styling

/* Placeholder shown where the item will be dropped */
.sortable-ghost {
    opacity: 0.4;
    background: #f0f9ff;
}

/* The element being dragged */
.sortable-chosen {
    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}

Or pass utility classes straight through the options:

wire:sortable.options='{"ghostClass": "opacity-30 bg-blue-50", "chosenClass": "shadow-xl"}'

Re-initializing

When adding sortable containers after the initial render:

document.dispatchEvent(new Event('reinit-sortable'));

Reaching SortableJS directly

import { Sortable, initSortable } from 'wire-sortable';

const el = document.getElementById('my-list');
Sortable.create(el, { /* options */ });

initSortable(); // re-initialize all wire:sortable elements

built and maintained by Edu Lazaro · MIT license