Wire Sortable
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