davidgut / sortable
A sortable trait for Laravel models
Requires
- php: ^8.2
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
README
Drag-and-drop sorting for your Eloquent models — backend and frontend included.
No dependencies, no build step required for the JS, just install and go.
Supports Laravel 11, 12 & 13.
Quick Start
composer require davidgut/sortable
Add a sort_order column to any table you want to sort:
$table->integer('sort_order')->nullable();
Then implement the contract on your model:
use DavidGut\Sortable\Contracts\Sortable; use DavidGut\Sortable\Traits\SortableTrait; class Post extends Model implements Sortable { use SortableTrait; }
That's it for the backend. New records automatically get the next sort order, and the package registers a PUT /sortable/{model}/{id} route for you.
Frontend
Publish the included JavaScript:
php artisan vendor:publish --tag=sortable-assets
Import it in your app.js:
import SortableList from './vendor/sortable/sortable'; SortableList.start();
Mark up your list:
<x-sortable::list> @foreach($posts as $post) <x-sortable::item :model="$post"> <x-sortable::drag /> {{ $post->title }} </x-sortable::item> @endforeach </x-sortable::list>
This produces:
<ul data-sortable> <li data-sortable-update-url="/sortable/Post/1"> <span class="drag">⠿</span> My First Post </li> ... </ul>
All three components accept as to change the rendered element (defaults: ul, li, span), and forward any extra attributes:
<x-sortable::list as="div" class="grid gap-2"> @foreach($posts as $post) <x-sortable::item as="div" :model="$post" class="card"> <x-sortable::drag as="button" class="cursor-grab">☰</x-sortable::drag> {{ $post->title }} </x-sortable::item> @endforeach </x-sortable::list>
Without Components
If you prefer plain HTML, use the @sortableUrl directive or the ->sortableUrl() method:
<ul data-sortable> @foreach($posts as $post) <li @sortableUrl($post)> <span class="drag">⠿</span> {{ $post->title }} </li> @endforeach </ul>
Make sure you have a <meta name="csrf-token"> tag in your layout — the JS reads it for requests.
Done. Your list is now sortable.
Configuration
Registering Models
Publish the config:
php artisan vendor:publish --tag=sortable-config
Map your models in config/sortable.php:
'models' => [ 'posts' => \App\Models\Post::class, ],
In non-production environments the package will also try to resolve
App\Models\{Name}automatically, so you can skip this step during development. In production, only explicitly registered models are allowed.
Custom Sort Column
The default column is sort_order. To change it, set the property on your model:
class Post extends Model implements Sortable { use SortableTrait; protected $sortColumn = 'order'; }
Scoped Sorting
Need separate sort orders per group? For example, sorting posts within each category independently:
class Post extends Model implements Sortable { use SortableTrait; protected ?string $sortScope = 'category_id'; }
Each category_id will now have its own sort sequence starting from 0.
Custom Sort Queries
For more advanced cases, override sortQuery():
protected function sortQuery(): Builder { return parent::sortQuery()->where('is_active', true); }
Custom Routes
The package auto-registers routes with web middleware. If you need to customise them:
php artisan vendor:publish --tag=sortable-routes
Query Scope
Retrieve records in sorted order:
Post::sorted()->get(); Post::sorted('desc')->get();
Authorization
By default, only users where $user->isAdmin() returns true can re-sort items. Override this per model:
public function canBeSortedBy($user): bool { return $user->id === $this->user_id; }
SPA / Livewire
SortableList.start() returns the created instances. Call SortableList.stop() to tear them down on navigation:
const instances = SortableList.start(); // Later, on page leave: SortableList.stop();
License
MIT