Translations of this page:
  • en

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 class
  • resources/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:

  1. Method 1: View-Based (Custom HTML/Blade)
  2. Method 2: Component-Based (Filament's built-in 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
                        ]),
                ]),
        ]);
}
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() (not getInfolist())
  • Check that you're calling this-_infolist in 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:

  1. Generate page with php artisan make:filament-page
  2. Use the infolist() method with Filament components
  3. Keep the Blade view simple: just this-_infolist
  4. Leverage built-in features: badges, icons, colors, copyable fields
  5. Add header actions for interactivity

This gives you consistency, maintainability, and all of Filament's built-in features with minimal code.

Additional Resources

Edit this page
Back to top