This guide explains how to create custom pages in Filament v4 that display a single, predetermined record. This is useful when you need to show specific data that doesn't fit the standard CRUD pattern, such as:
Use Artisan to generate a new Filament page:
php artisan make:filament-page BestEmployeeEver
This command creates two files:
app/Filament/Pages/BestEmployeeEver.php - The page classresources/views/filament/pages/best-employee-ever.blade.php - The view template<note tip> If you're using a custom panel (not the default admin panel), add the panel name:
php artisan make:filament-page BestEmployeeEver --panel=admin
</note>
Open app/Filament/Pages/BestEmployeeEver.php and configure it:
<?php namespace App\Filament\Pages; use Filament\Pages\Page; use App\Models\User; class BestEmployeeEver extends Page { // Icon displayed in navigation (uses Heroicons) protected static ?string $navigationIcon = 'heroicon-o-star'; // Path to the Blade view file protected static string $view = 'filament.pages.best-employee-ever'; // Label shown in navigation menu protected static ?string $navigationLabel = 'Best Employee'; // Page title displayed at the top protected static ?string $title = 'Best Employee Ever'; // Optional: Group pages together in navigation protected static ?string $navigationGroup = 'Employees'; // Optional: Control navigation order (lower numbers appear first) protected static ?int $navigationSort = 1; // Public property to hold the employee data public ?User $employee = null; /** * Mount method runs when the page loads * This is where you fetch your data */ public function mount(): void { // Fetch the specific employee by ID $this->employee = User::find(42); // Replace with your ID // Handle case where employee doesn't exist if (!$this->employee) { abort(404, 'Employee not found'); } } /** * Optional: Pass data to the view * Use this if you need additional processing */ protected function getViewData(): array { return [ 'employee' => $this->employee, // Add any other computed data here ]; } }
| Property | Type | Purpose |
|---|---|---|
$navigationIcon | string | Icon from Heroicons (heroicon-o-* for outline, heroicon-s-* for solid) |
$view | string | Path to Blade template (dots replace slashes) |
$navigationLabel | string | Text shown in sidebar navigation |
$title | string | Page heading displayed at top |
$navigationGroup | string | Groups related pages in navigation |
$navigationSort | int | Controls order in navigation (ascending) |
Edit resources/views/filament/pages/best-employee-ever.blade.php:
<x-filament-panels::page> <div class="space-y-6"> {{-- Hero Section with Employee Photo/Avatar --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-content p-6"> <div class="flex items-center gap-4"> @if($employee->avatar ?? null) <img src="{{ $employee->avatar }}" alt="{{ $employee->name }}" class="h-24 w-24 rounded-full object-cover"> @else <div class="flex h-24 w-24 items-center justify-center rounded-full bg-primary-500 text-3xl font-bold text-white"> {{ substr($employee->name, 0, 1) }} </div> @endif <div class="flex-1"> <h2 class="text-2xl font-bold text-gray-950 dark:text-white"> {{ $employee->name }} </h2> <p class="text-sm text-gray-500 dark:text-gray-400"> {{ $employee->email }} </p> </div> </div> </div> </div> {{-- Two Column Layout for Details --}} <div class="grid gap-6 md:grid-cols-2"> {{-- Personal Information Card --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold text-gray-950 dark:text-white"> Personal Information </h3> </div> <div class="fi-section-content p-6 pt-0"> <dl class="space-y-4"> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Full Name</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->name }}</dd> </div> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Email</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->email }}</dd> </div> @if($employee->phone ?? null) <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Phone</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->phone }}</dd> </div> @endif @if($employee->department ?? null) <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Department</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white">{{ $employee->department }}</dd> </div> @endif </dl> </div> </div> {{-- Statistics Card --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold text-gray-950 dark:text-white"> Statistics </h3> </div> <div class="fi-section-content p-6 pt-0"> <dl class="space-y-4"> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Member Since</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white"> {{ $employee->created_at->format('F j, Y') }} </dd> </div> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Years of Service</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white"> {{ $employee->created_at->diffInYears(now()) }} years </dd> </div> <div> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Projects</dt> <dd class="mt-1 text-sm text-gray-950 dark:text-white"> {{ $employee->projects->count() ?? 0 }} </dd> </div> </dl> </div> </div> </div> {{-- Full Width Description Section --}} <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold text-gray-950 dark:text-white"> Why This Employee is the Best </h3> </div> <div class="fi-section-content p-6 pt-0"> <p class="text-sm text-gray-700 dark:text-gray-300"> {{ $employee->bio ?? 'This employee has demonstrated exceptional performance, dedication, and commitment to excellence.' }} </p> </div> </div> </div> </x-filament-panels::page>
<x-filament-panels::page> <!-- Your content here --> </x-filament-panels::page>
This component provides the standard Filament page layout with proper styling and dark mode support.
Filament uses specific CSS classes that integrate with its design system:
| Class | Purpose |
|---|---|
fi-section | Container for content sections |
fi-section-header | Header area of a section |
fi-section-content | Content area of a section |
space-y-6 | Vertical spacing between elements |
dark:bg-gray-900 | Dark mode background |
dark:text-white | Dark mode text color |
If you want the page accessible via URL but not shown in the sidebar:
protected static bool $shouldRegisterNavigation = false;
Change the URL path for the page:
protected static string $slug = 'our-best-employee'; // Access via: /admin/our-best-employee
Show a badge next to the navigation item:
public static function getNavigationBadge(): ?string { return 'Featured'; } protected static ?string $navigationBadgeColor = 'success';
Control who can view the page:
public static function canAccess(): bool { return auth()->user()->can('view_best_employee'); }
Instead of hardcoding the employee ID, use environment configuration:
Add to .env:
BEST_EMPLOYEE_ID=42
In your page class:
public function mount(): void { $employeeId = env('BEST_EMPLOYEE_ID', 1); $this->employee = User::find($employeeId); }
Add to config/app.php:
'best_employee_id' => env('BEST_EMPLOYEE_ID', 1),
In your page class:
public function mount(): void { $this->employee = User::find(config('app.best_employee_id')); }
Create a settings table and fetch dynamically:
public function mount(): void { $employeeId = Setting::where('key', 'best_employee_id')->value('value'); $this->employee = User::find($employeeId); }
Show related models (e.g., projects, tasks):
public function mount(): void { $this->employee = User::with(['projects', 'tasks', 'achievements']) ->find(42); }
In your view:
@if($employee->projects->isNotEmpty()) <div class="fi-section rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5"> <div class="fi-section-header flex items-center gap-x-3 px-6 py-4"> <h3 class="text-base font-semibold">Recent Projects</h3> </div> <div class="fi-section-content p-6 pt-0"> <ul class="space-y-2"> @foreach($employee->projects as $project) <li class="text-sm">{{ $project->name }}</li> @endforeach </ul> </div> </div> @endif
Calculate data on-the-fly:
public function getCompletedProjectsProperty(): int { return $this->employee->projects() ->where('status', 'completed') ->count(); }
Access in view:
{{ $this->completedProjects }}
Include interactive buttons:
// In your page class use Filament\Actions\Action; protected function getHeaderActions(): array { return [ Action::make('sendCongratulations') ->label('Send Congratulations') ->icon('heroicon-o-envelope') ->action(function () { // Send email logic Notification::make() ->title('Congratulations sent!') ->success() ->send(); }), ]; }
Check:
$shouldRegisterNavigation is not set to falsecanAccess() method)php artisan filament:cache-components$view property<x-filament-panels::page>fi-section, etc.) for consistencydark:*)mount() method is publicHere's a full working example for a “Company Statistics” page:
Page Class: app/Filament/Pages/CompanyStats.php
<?php namespace App\Filament\Pages; use Filament\Pages\Page; use App\Models\Company; use App\Models\Order; class CompanyStats extends Page { protected static ?string $navigationIcon = 'heroicon-o-chart-bar'; protected static string $view = 'filament.pages.company-stats'; protected static ?string $title = 'Company Statistics'; public ?Company $company = null; public int $totalOrders = 0; public float $totalRevenue = 0; public function mount(): void { // Get the main company record (ID = 1) $this->company = Company::find(1); if (!$this->company) { abort(404, 'Company not found'); } // Calculate statistics $this->totalOrders = Order::count(); $this->totalRevenue = Order::sum('total_amount'); } }
View: resources/views/filament/pages/company-stats.blade.php
<x-filament-panels::page> <div class="grid gap-6 md:grid-cols-3"> <!-- Company Name Card --> <div class="fi-section rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5"> <h3 class="text-sm font-medium text-gray-500">Company Name</h3> <p class="mt-2 text-2xl font-bold text-gray-950">{{ $company->name }}</p> </div> <!-- Total Orders Card --> <div class="fi-section rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5"> <h3 class="text-sm font-medium text-gray-500">Total Orders</h3> <p class="mt-2 text-2xl font-bold text-gray-950">{{ number_format($totalOrders) }}</p> </div> <!-- Revenue Card --> <div class="fi-section rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5"> <h3 class="text-sm font-medium text-gray-500">Total Revenue</h3> <p class="mt-2 text-2xl font-bold text-gray-950">${{ number_format($totalRevenue, 2) }}</p> </div> </div> </x-filament-panels::page>
To create a custom page displaying a single record in Filament v4:
php artisan make:filament-pagemount() methodThis pattern works for any scenario where you need to display predetermined, specific data outside of standard CRUD operations.