Creating Custom Pages in Filament v4 to Display Single Records
Overview
This guide explains how to create custom pages in Filament v4 that display a single, predetermined record using Filament's built-in components. This is useful when you need to show specific data that doesn't fit the standard CRUD pattern, such as:
- Featured employee/user profiles
- System configuration displays
- Dashboard-style pages with specific data
- Report pages for particular records
Prerequisites
- Laravel application with Filament v4 installed
- Basic understanding of Laravel Blade templates
- Familiarity with Eloquent models
Step 1: Generate the Custom Page
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>
Step 2: Configure the Page Class (Component-Based Approach)
There are two main approaches to building custom pages in Filament:
- Method 1: View-Based (Custom HTML/Blade)
- Method 2: Component-Based (Filament's built-in components) ← RECOMMENDED
Method 2: Using Filament Components (Recommended)
This approach uses Filament's built-in components for consistency and maintainability:
<?php namespace App\Filament\Pages; use Filament\Pages\Page; use App\Models\User; use Filament\Support\Enums\FontWeight; use Filament\Infolists\Infolist; use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\ImageEntry; use Filament\Infolists\Components\Section; use Filament\Infolists\Components\Grid; class BestEmployeeEver extends Page { protected static ?string $navigationIcon = 'heroicon-o-star'; protected static string $view = 'filament.pages.best-employee-ever'; protected static ?string $navigationLabel = 'Best Employee'; protected static ?string $title = 'Best Employee Ever'; protected static ?string $navigationGroup = 'Employees'; public ?User $employee = null; public function mount(): void { // Fetch the specific employee $this->employee = User::with(['projects'])->find(42); if (!$this->employee) { abort(404, 'Employee not found'); } } /** * Define the infolist to display employee data * This uses Filament's built-in components */ public function infolist(Infolist $infolist): Infolist { return $infolist ->record($this->employee) ->schema([ // Hero Section with Avatar Section::make() ->schema([ Grid::make(3) ->schema([ ImageEntry::make('avatar') ->label('') ->circular() ->defaultImageUrl(function ($record) { return 'https://ui-avatars.com/api/?name=' . urlencode($record->name) . '&color=7F9CF5&background=EBF4FF'; }) ->size(96), Grid::make(1) ->schema([ TextEntry::make('name') ->label('') ->size(TextEntry\TextEntrySize::Large) ->weight(FontWeight::Bold), TextEntry::make('email') ->label('') ->icon('heroicon-m-envelope') ->color('gray'), ]) ->columnSpan(2), ]), ]) ->columnSpan('full'), // Personal Information Section Section::make('Personal Information') ->schema([ TextEntry::make('name') ->label('Full Name'), TextEntry::make('email') ->label('Email Address') ->icon('heroicon-m-envelope') ->copyable(), TextEntry::make('phone') ->label('Phone Number') ->icon('heroicon-m-phone') ->default('N/A'), TextEntry::make('department') ->label('Department') ->default('N/A') ->badge(), ]) ->columns(2) ->columnSpan(1), // Statistics Section Section::make('Statistics') ->schema([ TextEntry::make('created_at') ->label('Member Since') ->date('F j, Y') ->icon('heroicon-m-calendar'), TextEntry::make('years_of_service') ->label('Years of Service') ->state(fn ($record) => $record->created_at->diffInYears(now())) ->suffix(' years') ->icon('heroicon-m-clock'), TextEntry::make('projects_count') ->label('Total Projects') ->state(fn ($record) => $record->projects->count()) ->icon('heroicon-m-briefcase') ->badge() ->color('success'), TextEntry::make('completed_projects') ->label('Completed Projects') ->state(fn ($record) => $record->projects->where('status', 'completed')->count()) ->icon('heroicon-m-check-circle') ->badge() ->color('primary'), ]) ->columns(2) ->columnSpan(1), // Bio/Description Section Section::make('Why This Employee is the Best') ->schema([ TextEntry::make('bio') ->label('') ->default('This employee has demonstrated exceptional performance, dedication, and commitment to excellence.') ->prose(), ]) ->columnSpan('full'), ]) ->columns(2); } }
Understanding Infolist Components
| Component | Purpose | Common Methods |
|---|---|---|
Section | Groups related fields with optional header | heading(), description(), columns() |
TextEntry | Displays text data | label(), icon(), color(), badge(), copyable() |
ImageEntry | Displays images | circular(), square(), size(), defaultImageUrl() |
Grid | Creates responsive column layouts | columns(), columnSpan() |
IconEntry | Shows icons (checkmarks, etc.) | boolean(), icon(), color() |
Step 3: Create the Blade View (Component-Based)
When using the component-based approach, your Blade view becomes very simple:
File: resources/views/filament/pages/best-employee-ever.blade.php
<x-filament-panels::page> {{ $this->infolist }} </x-filament-panels::page>
That's it! The infolist() method in your page class handles all the rendering.
Alternative: View-Based Approach (More Control)
If you need more custom layouts that don't fit Filament's component structure, you can use the view-based approach:
Method 1: Custom HTML/Blade (For Unique Layouts)
<?php namespace App\Filament\Pages; use Filament\Pages\Page; use App\Models\User; class BestEmployeeEver extends Page { protected static ?string $navigationIcon = 'heroicon-o-star'; protected static string $view = 'filament.pages.best-employee-ever'; protected static ?string $navigationLabel = 'Best Employee'; protected static ?string $title = 'Best Employee Ever'; public ?User $employee = null; public function mount(): void { $this->employee = User::find(42); if (!$this->employee) { abort(404, 'Employee not found'); } } }
View file:
<x-filament-panels::page> <div class="space-y-6"> {{-- Use Filament's CSS classes for consistency --}} <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"> <!-- Your custom HTML here --> </div> </div> </div> </x-filament-panels::page>
Comparison: Component-Based vs View-Based
| Aspect | Component-Based (Infolist) | View-Based (Custom HTML) |
|---|---|---|
| Consistency | ✅ Perfect match with Filament | ⚠️ Manual styling needed |
| Maintenance | ✅ Auto-updates with Filament | ❌ Manual updates required |
| Flexibility | ⚠️ Limited to component structure | ✅ Complete control |
| Code Amount | ✅ Less code | ❌ More code |
| Learning Curve | ⚠️ Must learn component API | ✅ Standard HTML/CSS |
| Dark Mode | ✅ Automatic | ⚠️ Manual implementation |
| Accessibility | ✅ Built-in | ⚠️ Manual implementation |
| Best For | Standard data display | Unique/complex layouts |
<note important> Recommendation: Use the Component-Based approach (Infolist) for most cases. Only use custom HTML when you need layouts that Filament components cannot achieve. </note>
Advanced Infolist Customizations
Adding Custom Computed Values
TextEntry::make('performance_score') ->label('Performance Score') ->state(function ($record) { // Calculate on the fly $completedProjects = $record->projects->where('status', 'completed')->count(); $totalProjects = $record->projects->count(); return $totalProjects > 0 ? round(($completedProjects / $totalProjects) * 100) : 0; }) ->suffix('%') ->badge() ->color(fn ($state) => $state >= 80 ? 'success' : ($state >= 60 ? 'warning' : 'danger')),
Conditional Display
TextEntry::make('bonus') ->label('Annual Bonus') ->money('USD') ->visible(fn ($record) => $record->bonus > 0),
Custom Formatting
TextEntry::make('last_login') ->label('Last Active') ->dateTime() ->since() // Shows "2 hours ago" format ->icon('heroicon-m-clock'),
Rich Text Display
TextEntry::make('achievements') ->label('Recent Achievements') ->listWithLineBreaks() // For array data ->bulleted() ->limitList(5),
Adding Actions to Entries
use Filament\Infolists\Components\Actions\Action; Section::make() ->schema([ TextEntry::make('email') ->label('Email') ->copyable() ->suffixAction( Action::make('sendEmail') ->icon('heroicon-m-envelope') ->action(function ($record) { // Send email logic }) ), ]),
Using Multiple Sections with Tabs
For more complex pages, you can organize data into tabs:
use Filament\Infolists\Components\Tabs; public function infolist(Infolist $infolist): Infolist { return $infolist ->record($this->employee) ->schema([ Tabs::make('Employee Details') ->tabs([ Tabs\Tab::make('Overview') ->schema([ // Overview fields ]), Tabs\Tab::make('Projects') ->schema([ // Project-related fields ]) ->badge(fn ($record) => $record->projects->count()), Tabs\Tab::make('Performance') ->schema([ // Performance metrics ]), ]), ]); }
Displaying Related Records
use Filament\Infolists\Components\RepeatableEntry; Section::make('Recent Projects') ->schema([ RepeatableEntry::make('projects') ->label('') ->schema([ TextEntry::make('name') ->label('Project Name') ->weight(FontWeight::Bold), TextEntry::make('status') ->badge() ->color(fn (string $state): string => match ($state) { 'completed' => 'success', 'in_progress' => 'warning', 'pending' => 'gray', default => 'danger', }), TextEntry::make('completion_date') ->date() ->label('Completed'), ]) ->columns(3), ]),
Page Configuration Options
Hide from Navigation
protected static bool $shouldRegisterNavigation = false;
Custom URL Slug
protected static string $slug = 'our-best-employee'; // Access via: /admin/our-best-employee
Add Navigation Badge
public static function getNavigationBadge(): ?string { return 'Featured'; } protected static ?string $navigationBadgeColor = 'success';
Restrict Access with Policies
public static function canAccess(): bool { return auth()->user()->can('view_best_employee'); }
Add Header Actions
use Filament\Actions\Action; use Filament\Notifications\Notification; protected function getHeaderActions(): array { return [ Action::make('sendCongratulations') ->label('Send Congratulations') ->icon('heroicon-o-envelope') ->color('success') ->requiresConfirmation() ->action(function () { // Send email logic here Notification::make() ->title('Congratulations sent!') ->success() ->send(); }), Action::make('exportProfile') ->label('Export Profile') ->icon('heroicon-o-arrow-down-tray') ->action(fn () => $this->exportEmployeeProfile()), ]; }
Dynamic Employee ID Configuration
Method 1: Environment Variable
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); }
Method 2: Config File
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')); }
Method 3: Database Setting
public function mount(): void { $employeeId = Setting::where('key', 'best_employee_id')->value('value'); $this->employee = User::find($employeeId); }
Complete Working Example
Here's a full working example using Filament components:
Page Class: app/Filament/Pages/TopPerformer.php
<?php namespace App\Filament\Pages; use Filament\Pages\Page; use App\Models\Employee; use Filament\Infolists\Infolist; use Filament\Infolists\Components\{TextEntry, ImageEntry, Section, Grid}; use Filament\Support\Enums\FontWeight; use Filament\Actions\Action; use Filament\Notifications\Notification; class TopPerformer extends Page { protected static ?string $navigationIcon = 'heroicon-o-trophy'; protected static string $view = 'filament.pages.top-performer'; protected static ?string $title = 'Top Performer of the Month'; public ?Employee $employee = null; public function mount(): void { $this->employee = Employee::with(['department', 'projects']) ->find(config('app.top_performer_id')); if (!$this->employee) { abort(404); } } public function infolist(Infolist $infolist): Infolist { return $infolist ->record($this->employee) ->schema([ Grid::make(3) ->schema([ ImageEntry::make('photo') ->label('') ->circular() ->size(120), Grid::make(1) ->schema([ TextEntry::make('full_name') ->label('') ->size(TextEntry\TextEntrySize::Large) ->weight(FontWeight::Bold), TextEntry::make('position') ->label('') ->badge() ->color('primary'), TextEntry::make('department.name') ->label('') ->icon('heroicon-m-building-office'), ]) ->columnSpan(2), ]) ->columnSpanFull(), Section::make('Performance Metrics') ->schema([ TextEntry::make('projects_completed') ->label('Projects Completed') ->state(fn ($record) => $record->projects->where('status', 'completed')->count()) ->badge() ->color('success'), TextEntry::make('satisfaction_score') ->suffix('%') ->color(fn ($state) => $state >= 90 ? 'success' : 'warning'), TextEntry::make('years_experience') ->label('Experience') ->state(fn ($record) => $record->hire_date->diffInYears(now())) ->suffix(' years'), ]) ->columns(3), ]); } protected function getHeaderActions(): array { return [ Action::make('recognize') ->label('Send Recognition') ->icon('heroicon-o-envelope') ->color('success') ->action(function () { Notification::make() ->title('Recognition sent!') ->success() ->send(); }), ]; } }
View: resources/views/filament/pages/top-performer.blade.php
<x-filament-panels::page> {{ $this->infolist }} </x-filament-panels::page>
Troubleshooting
Infolist Not Rendering
- Ensure the method is named
infolist()(notgetInfolist()) - Check that you're calling
this-_infolistin the view - Verify all required imports are present
Styling Issues
- Filament components handle styling automatically
- Don't mix custom CSS with component-based approach
- Use component methods (
color(),badge(), etc.) for styling
Data Not Displaying
- Verify the
record()is set on the infolist - Check that relationship names match your Eloquent models
- Use
state()callback for computed values
When to Use Each Approach
Use Component-Based (Infolist):
- Standard data display pages
- Profile pages
- Detail views
- Report pages with structured data
- When you want automatic dark mode and accessibility
Use View-Based (Custom HTML):
- Highly custom layouts (e.g., magazine-style designs)
- Complex dashboards with unique visualization
- Integration with third-party JavaScript libraries
- When Filament components don't support your design
Summary
Recommended Approach:
- Generate page with
php artisan make:filament-page - Use the
infolist()method with Filament components - Keep the Blade view simple: just
this-_infolist - Leverage built-in features: badges, icons, colors, copyable fields
- Add header actions for interactivity
This gives you consistency, maintainability, and all of Filament's built-in features with minimal code.

