Skip to content

Filament v3 Infolist Entries: Building Rich Read-Only Detail Pages Without Blade Sprawl

Filament's Infolist API lets you compose structured, read-only detail views entirely in PHP. Learn how to build custom entries, nest repeatable sections, and wire conditional visibility to Eloquent state.

Published · Updated · 3 min read

Filament v3 Infolist Entries: Building Rich Read-Only Detail Pages Without Blade Sprawl

Why the Infolist API Exists

Before Filament introduced the Infolist API, displaying a record's details meant either duplicating form schema into a disabled form or falling back to a custom Blade view. Both approaches break down fast: disabled forms carry validation baggage, and raw Blade views drift out of sync with your resource schema.

The Infolist API gives you a first-class, composable, read-only rendering layer that mirrors the form schema API in structure but is purpose-built for display. This article focuses on the practical patterns that matter in production: custom entries, repeatable sections, and state-driven visibility.


Registering an Infolist on a Resource

Add infolist() to your ViewRecord page or directly on the resource:

// app/Filament/Resources/OrderResource.php

public static function infolist(Infolist $infolist): Infolist
{
    return $infolist->schema([
        Section::make('Customer')
            ->columns(2)
            ->schema([
                TextEntry::make('customer.name')->label('Name'),
                TextEntry::make('customer.email')->label('Email'),
            ]),

        Section::make('Financials')
            ->schema([
                TextEntry::make('total_amount')
                    ->money('USD')
                    ->label('Total'),
                TextEntry::make('status')
                    ->badge()
                    ->color(fn (string $state): string => match ($state) {
                        'paid'    => 'success',
                        'pending' => 'warning',
                        default   => 'danger',
                    }),
            ]),
    ]);
}

The ->badge() modifier on TextEntry renders the value as a coloured pill — no custom Blade required.


Building a Custom Entry

When built-in entries aren't enough, extend Entry directly. Here's a JsonTreeEntry that renders a JSONB column as an indented key-value list:

// app/Filament/Infolists/Components/JsonTreeEntry.php

namespace App\Filament\Infolists\Components;

use Filament\Infolists\Components\Entry;

class JsonTreeEntry extends Entry
{
    protected string $view = 'filament.infolists.components.json-tree-entry';

    public function getState(): array
    {
        $raw = parent::getState();
        return is_array($raw) ? $raw : json_decode($raw, true) ?? [];
    }
}
{{-- resources/views/filament/infolists/components/json-tree-entry.blade.php --}}
<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
    <ul class="text-sm space-y-1">
        @foreach ($getState() as $key => $value)
            <li><span class="font-medium">{{ $key }}:</span> {{ $value }}</li>
        @endforeach
    </ul>
</x-dynamic-component>

Register it as a static factory method for ergonomics:

JsonTreeEntry::make('meta')->label('Metadata'),

Repeatable Sections for HasMany Relations

RepeatableEntry iterates a relationship and renders a schema for each child record:

use Filament\Infolists\Components\RepeatableEntry;

RepeatableEntry::make('lineItems')
    ->label('Line Items')
    ->schema([
        TextEntry::make('product.name')->label('Product'),
        TextEntry::make('quantity'),
        TextEntry::make('unit_price')->money('USD'),
    ])
    ->columns(3),

Filament eager-loads the relationship automatically when the infolist is resolved, so there's no N+1 concern here as long as the relationship is declared on the model.


Conditional Visibility Tied to Eloquent State

Use ->visible() or ->hidden() with a closure that receives the record state:

TextEntry::make('refund_reason')
    ->label('Refund Reason')
    ->visible(fn (Order $record): bool => $record->status === 'refunded'),

Section::make('Fraud Flags')
    ->schema([
        TextEntry::make('fraud_score'),
        TextEntry::make('flagged_at')->dateTime(),
    ])
    ->hidden(fn (Order $record): bool => ! $record->is_flagged),

The closure receives the full Eloquent model, so you can call any method or accessor — no need to push display logic into the controller.


Takeaways

  • The Infolist API is a dedicated read-only layer; don't repurpose disabled forms for detail views.
  • Custom entries need only a view and an optional getState() override — the wrapper component handles label, help text, and layout automatically.
  • RepeatableEntry handles HasMany relationships cleanly; Filament manages eager loading.
  • ->visible() / ->hidden() closures receive the Eloquent record, keeping conditional display logic close to the schema definition.
  • Badge colours on TextEntry are resolved per-row via closures, making status indicators trivial to implement without custom columns.

Frequently asked questions

Can I reuse my form schema inside an infolist to avoid duplication?

Not directly — form components and infolist entries are separate class hierarchies. However, you can extract shared configuration (labels, column counts) into static methods on the resource and call them from both `form()` and `infolist()` to reduce duplication.

Does RepeatableEntry trigger N+1 queries for nested relationships?

Filament resolves the relationship through Eloquent's standard eager loading when it hydrates the infolist record. As long as the relationship is defined on the model, Filament will load it in a single query. For deeply nested relations, add explicit `->with()` calls in your resource's `getEloquentQuery()` method.

How do I add actions (like a copy button) inside a custom entry?

Custom entries can render any Blade, including Livewire-compatible Alpine components. For a copy button, add an Alpine `x-on:click` that writes to the clipboard inside your entry view. Full Filament actions inside infolist entries require using `Actions\Action` registered on the infolist itself via `->headerActions()` or `->footerActions()` on a Section.

Mohamed Said

About the author

Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

About GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

Tell me what you’re building. I reply within two working days.

Discuss your project