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.
composer require davidgut/sortableAdd 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.
Publish the included JavaScript:
php artisan vendor:publish --tag=sortable-assetsImport 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>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.
Publish the config:
php artisan vendor:publish --tag=sortable-configMap 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.
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';
}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.
For more advanced cases, override sortQuery():
protected function sortQuery(): Builder
{
return parent::sortQuery()->where('is_active', true);
}The package auto-registers routes with web middleware. If you need to customise them:
php artisan vendor:publish --tag=sortable-routesRetrieve records in sorted order:
Post::sorted()->get();
Post::sorted('desc')->get();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;
}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();MIT