davidgut/sortable

A sortable trait for Laravel models

Maintainers

Package info

github.com/davidgut/sortable

pkg:composer/davidgut/sortable

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v3.1.0 2026-03-26 22:21 UTC

This package is auto-updated.

Last update: 2026-03-27 16:33:43 UTC


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