ibrahimbougaoua/filament-sort-order
filament-sort-order is a Laravel/Filament package that adds drag-and-drop sorting to Filament tables. Configure an order column, enable reordering in your resource, and persist row positions automatically.
Install the package via Composer and let Filament's plugin auto-discovery do its work:
composer require ibrahimbougaoua/filament-sort-order
Check config/filament-sort-order.php after install (publish it with php artisan vendor:publish --tag="filament-sort-order-config" if the package ships one) — this is where the sort column name and any global toggles typically live.
Add an order column to the model's table if you don't already have one. The description says “no extra migrations required,” but that means no package-specific migrations — you still need a column to persist to:
$table->unsignedInteger('sort_order')->default(0)->index();
A nullable or defaulted integer is fine; the package writes the new values on drop.
Enable reordering on the table in your Filament resource. The first thing to check is the resource's table() method — that's where every configuration example in the package's README will live:
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name'),
])
->defaultSort('sort_order')
->reorderable('sort_order'); // column name matches your migration
}
Your first use case: a Category resource where editors control navigation order. Seed a few rows with sequential sort_order values, drag one row above another in the admin panel, refresh, and confirm the persisted order changed. That single end-to-end loop is the whole value proposition — verify it before wiring it into anything complex.
Where to look first if something is off: the resource's table() signature (does it expect ->reorderable() or a plugin registration like ->plugin(SortOrderPlugin::make())?), then the published config for the column name, then the browser network tab for the PATCH/POST request fired on drop. With a 22-star package, the README is the source of truth and may lag behind the code — read the actual src/ classes if the documented method names don't exist on your Filament version.
Default-sort everything. The single most important pattern: any table using reordering should also have ->defaultSort('sort_order'). Without it, the rows render in whatever order the database returns, so the drag-and-drop UI appears to “lose” the order after a reload even though the write succeeded.
Scope ordering to a parent. For menus, FAQ sections, or anything nested, the sort is almost always relative to a parent. Use Filament's scoped reordering so positions are (parent_id, sort_order) rather than global:
->reorderable('sort_order', condition: fn (Builder $query) => $query->where('menu_id', $this->menuId))
If the package doesn't expose a condition parameter, handle scoping in getEloquentQuery() on the resource and confirm the package reads the filtered query when computing positions.
Combine with the standard Filament reordering trait for new records. Dragging only fixes existing rows. New rows still need a position assigned on create. In the model (or an observer):
protected static function booted(): void
{
static::creating(function ($model) {
$model->sort_order ??= (static::max('sort_order') ?? 0) + 1;
});
}
This keeps appends at the end rather than all collapsing to 0.
Use it inside Relation Managers too. Relation manager tables are just tables, so the same ->reorderable() call applies — this is the natural fit for “FAQ items within an FAQ category” where you don't want a separate resource per child.
Respect policies. The description notes it “works with existing policies,” which means you should verify it actually checks update before persisting. If it doesn't, wrap the table visibility yourself (->reorderable(fn () => auth()->user()->can('update', $record))) so read-only roles can't reorder.
Pair with ->paginated(false) for small lists. Drag-and-drop across paginated pages is a known limitation in every Filament reorder implementation — the UI only reorders the visible page. For short lists (categories, statuses, nav items), disabling pagination gives editors the full picture and avoids surprise “positions reset on page 2” bugs.
Seeding and imports should always set sort_order deterministically ($index + 1 in the seeder loop), because the drag handler assumes the column is populated and unique-ish. Gaps are fine; duplicates cause ambiguity on the next drag.
sort_order (common after manual DB edits or bulk inserts with defaults), a drag may only update one and leave rows visually “stuck.” Run a one-off php artisan tinker cleanup to renumber sequentially before handing the feature to editors.sort_order. Don't add one — reordering rewrites several rows in sequence, and a unique constraint will throw mid-transaction. Use a plain index instead.->reorderable() is built into the table builder natively, so you may not need the package at all. Verify compatibility before committing to it in a new project.updated event or the package's persistence point to bust the cache. Otherwise editors reorder, see it in the admin, and report “the frontend is broken.”sort_order values), and leave the actual drag UX to manual QA.->reorderable() call (per-resource opt-in), and the model itself (new-record positioning). Those three cover 95% of customization — if you find yourself patching vendor code, you're probably fighting a missing default sort or a missing scope rather than a package limitation.How can I help you explore Laravel packages today?