A Laravel package that adds elegant next/previous record navigation to your Filament PHP admin panels. Navigate seamlessly between records with intuitive navigation buttons.
getHeaderActions() and you're doneAction class, all fluent methods workview, edit, or any custom route as the target page per actiongetPages()$livewire injection API^8.2^10.0 | ^11.0 | ^12.0 | ^13.0^4.0 | ^5.0Live site: rnd.nben.com.np

Install the package via Composer:
composer require nben/filament-record-nav
Optionally publish the config file to customise the order column and sort directions:
php artisan vendor:publish --tag=filament-record-nav-config
Add the two actions to getHeaderActions() on any ViewRecord or EditRecord page.
No trait required - this is all you need for the default behaviour.
<?php
namespace App\Filament\Resources\PostResource\Pages;
use App\Filament\Resources\PostResource;
use Filament\Resources\Pages\ViewRecord;
use Nben\FilamentRecordNav\Actions\NextRecordAction;
use Nben\FilamentRecordNav\Actions\PreviousRecordAction;
class ViewPost extends ViewRecord
{
protected static string $resource = PostResource::class;
protected function getHeaderActions(): array
{
return [
PreviousRecordAction::make(),
NextRecordAction::make(),
// ... your other actions
];
}
}
Out of the box the actions will:
order_column defined in config (default: id)view page of the adjacent recordUse ->navigateTo() with the NavigationPage enum to control which page type
the action redirects to. Each action can target a different page type independently.
use Nben\FilamentRecordNav\Actions\NextRecordAction;
use Nben\FilamentRecordNav\Actions\PreviousRecordAction;
use Nben\FilamentRecordNav\Enums\NavigationPage;
protected function getHeaderActions(): array
{
return [
// Both navigate to the edit page
PreviousRecordAction::make()->navigateTo(NavigationPage::Edit),
NextRecordAction::make()->navigateTo(NavigationPage::Edit),
];
}
Or mix them:
// Previous stays on view, next goes to edit
PreviousRecordAction::make()->navigateTo(NavigationPage::View),
NextRecordAction::make()->navigateTo(NavigationPage::Edit),
If your resource registers custom page routes beyond the standard view and edit,
use NavigationPage::custom() to navigate to them by route name.
First, register your custom page in the resource's getPages():
public static function getPages(): array
{
return [
'index' => ListUsers::route('/'),
'create' => CreateUser::route('/create'),
'view' => ViewUser::route('/{record}'),
'edit' => EditUser::route('/{record}/edit'),
'verified-view' => ViewVerifiedUser::route('/{record}/verified'), // custom route
];
}
Then pass the route name string to NavigationPage::custom():
use Nben\FilamentRecordNav\Actions\NextRecordAction;
use Nben\FilamentRecordNav\Actions\PreviousRecordAction;
use Nben\FilamentRecordNav\Enums\NavigationPage;
protected function getHeaderActions(): array
{
return [
PreviousRecordAction::make()->navigateTo(NavigationPage::custom('verified-view')),
NextRecordAction::make()->navigateTo(NavigationPage::custom('verified-view')),
];
}
The route name passed to custom() is forwarded directly to Resource::getUrl($routeName, ['record' => $record]),
so it must exactly match a key in your resource's getPages() array.
Note: Custom routes still benefit from the same smart boundary detection - buttons auto-disable and turn gray when there is no adjacent record to navigate to.
Both actions extend Filament's Action class directly, so every fluent method
from the Filament Actions docs
works as normal:
use Filament\Support\Enums\Size;
use Nben\FilamentRecordNav\Actions\NextRecordAction;
use Nben\FilamentRecordNav\Actions\PreviousRecordAction;
PreviousRecordAction::make()
->label('← Previous')
->color('secondary')
->size(Size::Small)
->tooltip('Go to previous record')
->keyBindings(['mod+left']),
NextRecordAction::make()
->label('Next →')
->icon('heroicon-o-arrow-right')
->color('secondary')
->size(Size::Small)
->tooltip('Go to next record')
->keyBindings(['mod+right']),
For custom query logic - filtering by status, scoping to a tenant, ordering by
a different column - add the WithRecordNavigation trait to your page class and
override the methods you need.
The trait is optional. Only add it when the default config-driven query is not sufficient.
<?php
namespace App\Filament\Resources\PostResource\Pages;
use App\Filament\Resources\PostResource;
use Filament\Resources\Pages\ViewRecord;
use Illuminate\Database\Eloquent\Model;
use Nben\FilamentRecordNav\Actions\NextRecordAction;
use Nben\FilamentRecordNav\Actions\PreviousRecordAction;
use Nben\FilamentRecordNav\Concerns\WithRecordNavigation;
class ViewPost extends ViewRecord
{
use WithRecordNavigation;
protected static string $resource = PostResource::class;
protected function getHeaderActions(): array
{
return [
PreviousRecordAction::make(),
NextRecordAction::make(),
];
}
// Only navigate through published posts, ordered by published_at
public function getPreviousRecord(): ?Model
{
return $this->getRecord()
->newQuery()
->where('status', 'published')
->where('published_at', '<', $this->getRecord()->published_at)
->orderBy('published_at', 'desc')
->first();
}
public function getNextRecord(): ?Model
{
return $this->getRecord()
->newQuery()
->where('status', 'published')
->where('published_at', '>', $this->getRecord()->published_at)
->orderBy('published_at', 'asc')
->first();
}
}
Override getRecordNavigationUrl() to fully control where the action redirects.
The $page parameter is either a NavigationPage enum case or a CustomNavigationPage
value object - both expose a ->value string with the route name.
use Illuminate\Database\Eloquent\Model;
use Nben\FilamentRecordNav\Enums\CustomNavigationPage;
use Nben\FilamentRecordNav\Enums\NavigationPage;
// Always navigate to the edit page, ignoring the action's NavigationPage setting
public function getRecordNavigationUrl(
Model $record,
NavigationPage|CustomNavigationPage $page
): string {
return static::getResource()::getUrl('edit', ['record' => $record]);
}
Or respect whatever page type was passed, including custom routes:
public function getRecordNavigationUrl(
Model $record,
NavigationPage|CustomNavigationPage $page
): string {
// $page->value is 'view', 'edit', or your custom route name
return static::getResource()::getUrl($page->value, ['record' => $record]);
}
If you want IDE autocompletion and static analysis support (PHPStan / Psalm),
declare implements HasRecordNavigation alongside the trait:
use Nben\FilamentRecordNav\Concerns\WithRecordNavigation;
use Nben\FilamentRecordNav\Contracts\HasRecordNavigation;
class ViewPost extends ViewRecord implements HasRecordNavigation
{
use WithRecordNavigation; // provides the default implementation
}
Note: The actions use
method_exists()rather thaninstanceofto detect navigation methods. This means declaringimplementsis optional - the actions will call your overridden methods correctly either way. The interface is purely for your own type-safety tooling.
// config/filament-record-nav.php
return [
/*
| The column used to determine record order for navigation.
| When navigating previous, queries for the nearest record with a lower
| value. When navigating next, queries for the nearest record with a higher
| value. Add a database index on this column for best performance.
|
| Common choices: 'id', 'created_at', 'updated_at', 'sort_order'
*/
'order_column' => 'id',
/*
| Sort directions for the previous and next queries.
| 'desc' for previous ensures the closest lower record is returned first.
| 'asc' for next ensures the closest higher record is returned first.
| Only change these if you have a non-standard ordering requirement.
*/
'previous_direction' => 'desc',
'next_direction' => 'asc',
];
src/
├── Actions/
│ ├── Concerns/
│ │ └── ResolvesAdjacentRecord.php ← shared cache, URL resolution, fallback query
│ ├── NextRecordAction.php ← thin action, delegates to the trait above
│ └── PreviousRecordAction.php ← thin action, delegates to the trait above
├── Concerns/
│ └── WithRecordNavigation.php ← optional page trait for custom query logic
├── Contracts/
│ └── HasRecordNavigation.php ← optional interface for strict typing
├── Enums/
│ ├── CustomNavigationPage.php ← value object for arbitrary route names
│ └── NavigationPage.php ← View | Edit | custom()
└── FilamentRecordNavServiceProvider.php
When a page renders, Filament evaluates each action's color(), disabled(),
and url() closures. Each closure receives the Livewire component instance via
Filament's $livewire utility injection - a stable, first-class API that is
not expected to change between Filament versions.
Inside each closure, ResolvesAdjacentRecord::getCachedRecord() is called.
On the first call it resolves the adjacent record (via the page's override or
the config fallback), stores it in $resolvedRecordCache, and returns it.
The second and third closure calls read from the cache - so only one database
query fires per action per render, regardless of how many closures need the result.
When navigateTo() receives a NavigationPage enum case or a CustomNavigationPage
value object, both expose the same ->value string property. resolveUrl() calls
Resource::getUrl($page->value, ['record' => $record]) on either type without any
instanceof branching, keeping the URL resolution path simple and uniform.
No internal Filament lifecycle hooks (configureAction(), bootUsing(), etc.)
are used anywhere in the package.
Buttons are always disabled (gray)
order_column in config matches an actual column on your table.created_at), ensure records have distinct values - records with identical timestamps will not find each other.getPreviousRecord() / getNextRecord(), add a temporary \Log::info(...) inside them to confirm they are being called and the query returns what you expect.Custom filter overrides are being ignored
WithRecordNavigation trait is present on the page class with use WithRecordNavigation;.public function getPreviousRecord(): ?Model and public function getNextRecord(): ?Model.use Illuminate\Database\Eloquent\Model; is imported at the top of the page class.Wrong page type after navigation
->navigateTo(NavigationPage::Edit) on the action for the edit page.->navigateTo(NavigationPage::custom('your-route-name')) for a custom route.custom() exactly matches a key in your resource's getPages() array.getRecordNavigationUrl() in the trait to always return the route you want.InvalidArgumentException or route not found after using NavigationPage::custom()
custom() must exactly match a key in getPages(), including hyphens and casing.'verified-view' => ViewVerifiedUser::route('/{record}/verified') in getPages() matches NavigationPage::custom('verified-view') on the action.Performance on large tables
order_column.getPreviousRecord() / getNextRecord() to add where() scopes that narrow the result set before the adjacent record query runs.v2.1.0 is fully backward compatible with v2.0.0. No changes are required to
existing code. The only addition is the NavigationPage::custom() named
constructor and the CustomNavigationPage value object that supports it.
v2.0.0 is a full rewrite for Filament v4/v5. Summary of breaking changes:
| v1.x | v2.x |
|---|---|
| Trait required on the page | Trait optional |
configureAction() hook used internally |
Removed - uses $livewire injection |
| No page-type enum | NavigationPage::View / NavigationPage::Edit / NavigationPage::custom() |
getRecordUrl(Model $record) |
getRecordNavigationUrl(Model $record, NavigationPage|CustomNavigationPage $page) |
Filament ^3.0 |
Filament ^4.0 | ^5.0 |
PHP ^8.1 |
PHP ^8.2 |
Migration steps:
configureAction() from any page classes that overrode it - it is no longer used.getRecordUrl(Model $record) to getRecordNavigationUrl(Model $record, NavigationPage|CustomNavigationPage $page) wherever you defined it. Import both NavigationPage and CustomNavigationPage at the top of the file.->navigateTo(NavigationPage::Edit) to any actions that previously pointed to edit routes via getRecordUrl().use WithRecordNavigation from pages that relied only on the default behaviour (no method overrides).Contributions are welcome. Please open an issue before submitting a pull request for major changes so we can discuss the approach first.
This package is open-sourced software licensed under the MIT license.
Made with ❤️ for the Filament PHP community
How can I help you explore Laravel packages today?