Filament v4 Infolists: Build Read-Only Record Views

Filament's default view page is just a greyed-out form. Build a read-only record view with a Filament infolist: TextEntry, IconEntry, sections, and modals.

Steven Richardson
Steven Richardson
· 7 min read

You add a View page to a Filament resource, click through, and it's an anticlimax: every field rendered as a greyed-out, disabled form input. It works, but it reads like a form someone forgot to switch on. For a detail screen you usually want the opposite — a clean read-only display with badges, money formatting, and a layout that groups related fields. That's what a Filament infolist gives you, and in v4 it's a Schema you define once and reuse. If you're standing up an entire panel rather than one screen, my complete Filament v4 dashboard build covers the surrounding setup; here I'm zooming into the record detail page.

Define a Filament infolist on your resource#

The default Filament view page displays a disabled form. To swap it for an infolist, add an infolist() method to your resource. This is the first thing that trips people up coming from v3: the method now receives a Filament\Schemas\Schema, not an Infolist, and you pass entries to ->components(), not ->schema().

use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;

public static function infolist(Schema $schema): Schema
{
    return $schema
        ->components([
            TextEntry::make('reference'),
            TextEntry::make('customer.name'),
            TextEntry::make('total'),
            TextEntry::make('notes')
                ->columnSpanFull(),
        ]);
}

Entry classes still live in Filament\Infolists\Components, and TextEntry::make() takes the attribute name. Dot notation (customer.name) reads through relationships, so you rarely need to touch the query. The value an entry shows is its "state" — Filament pulls it from the record automatically, the same way a form field would. This is the inverse of building a custom form field: same schema plumbing, but read-only.

Add entries, badges, and formatting#

Raw values are rarely what you want on a detail screen. Entries carry the formatting helpers, so a status becomes a coloured pill and a total becomes currency without a single accessor on the model.

use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\TextEntry;

TextEntry::make('status')
    ->badge()
    ->color(fn (string $state): string => match ($state) {
        'pending' => 'warning',
        'paid' => 'success',
        'cancelled' => 'danger',
        default => 'gray',
    });

TextEntry::make('total')
    ->money('GBP'); // value stored as a decimal

TextEntry::make('total_pennies')
    ->money('GBP', divideBy: 100); // value stored as integer minor units

TextEntry::make('created_at')
    ->dateTime('j M Y, H:i');

IconEntry::make('is_paid')
    ->boolean(); // green check / red cross instead of "1" / "0"

TextEntry also gives you ->copyable(), ->limit(), ->html(), and ->listWithLineBreaks() for array state. When the data is a repeatable set of rows — order lines, addresses — reach for RepeatableEntry, the read-only mirror of the editable Repeater and Builder for nested form data.

Lay out the infolist with sections and grids#

A flat list of entries works for three fields and falls apart at thirty. Layout components group and arrange them. The catch in v4: layout components moved into the Filament\Schemas\Components namespace (they were under Infolists in v3), and inside a layout component you go back to ->schema() — only the top-level Schema object uses ->components().

use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;

public static function infolist(Schema $schema): Schema
{
    return $schema
        ->components([
            Section::make('Order')
                ->description('Reference and current status')
                ->columns(2)
                ->schema([
                    TextEntry::make('reference')->copyable(),
                    TextEntry::make('status')->badge(),
                    TextEntry::make('total')->money('GBP'),
                    TextEntry::make('created_at')->dateTime('j M Y'),
                ]),
            Section::make('Customer')
                ->schema([
                    TextEntry::make('customer.name'),
                    TextEntry::make('customer.email')->copyable(),
                ]),
        ]);
}

->columns(2) sets the grid at the lg breakpoint and up, collapsing to one column on small screens. Grid, Tabs, and Fieldset live in the same namespace and compose the same way when a record needs more structure.

Reuse one Filament infolist in a table modal#

If a resource is simple, you may not want a separate view page at all — a modal launched from the table row is enough. The trick is to define the entries once and feed them to both places. Extract them into a static method that returns an array of components:

use Filament\Actions\ViewAction;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Table;

/** @return array<\Filament\Schemas\Components\Component> */
public static function detailComponents(): array
{
    return [
        Section::make('Order')
            ->columns(2)
            ->schema([
                TextEntry::make('reference')->copyable(),
                TextEntry::make('status')->badge(),
                TextEntry::make('total')->money('GBP'),
                TextEntry::make('created_at')->dateTime('j M Y'),
            ]),
    ];
}

public static function infolist(Schema $schema): Schema
{
    return $schema->components(static::detailComponents());
}

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->recordActions([
            ViewAction::make()
                ->schema(static::detailComponents()),
        ]);
}

Now the View page and the row-level ViewAction modal render identical detail, and there's one place to change it. Note recordActions() — that's the v4 name for what v3 called actions() on the table. If you want the modal to do more than display, the same action API drives custom actions with confirmation modals and notifications.

Gotchas and edge cases#

The single most common v4 error is mixing up ->components() and ->schema(). The top-level Schema object passed to infolist() takes ->components(); layout components like Section and Grid take ->schema(). Swap them and you get a confusing "method not found" or a silently empty panel.

Namespaces are the other v3 trap. Entries stayed in Filament\Infolists\Components, but the method parameter is now Filament\Schemas\Schema and layout components are in Filament\Schemas\Components. Pasting a v3 infolist wholesale will fail to resolve classes until you fix the imports.

Conditional visibility has a performance edge. A ->hidden(fn (Get $get) => ...) closure that depends on another field only re-evaluates when the schema does a server round-trip, and only if that field is live() — the same reactivity model behind dependent, reactive select fields. For pure show/hide based on another value, ->hiddenJs() and ->visibleJs() run client-side and skip the request entirely.

Finally, icons are enums now. Pass Filament\Support\Icons\Heroicon cases (Heroicon::CheckCircle) rather than the old 'heroicon-o-check-circle' strings. And know when not to bother: if all you need is a quick disabled view and you don't care about presentation, the default disabled form is fine — the infolist is for when the detail screen is something users actually look at.

Wrapping up#

Define infolist() on the resource, build it from entries in ->components(), group them with sections from the Schemas namespace, and extract the components into a static method the moment you need the same view in a modal. That covers the large majority of read-only record detail pages you'll ship.

From here, two natural next steps: surface related records on the same screen with nested resources for parent-child models, or move read-only summaries onto the dashboard itself with a stats overview widget.

FAQ#

What is an infolist in Filament?

An infolist is Filament's read-only counterpart to a form. Where a form collects input through editable fields, an infolist displays a record's data through entries such as TextEntry and IconEntry. It's used on resource view pages, inside action modals, on dashboards, and in relation managers — anywhere you want to present data rather than edit it.

How do I create a view page in Filament v4?

Generate a resource with the --view flag (php artisan make:filament-resource Order --view), or add a page to an existing resource with php artisan make:filament-page ViewOrder --resource=OrderResource --type=ViewRecord and register it in the resource's getPages() method. By default the page shows a disabled form; define an infolist() method on the resource to display an infolist instead.

What is the difference between a form and an infolist in Filament?

A form is editable and writes data back to the model through fields, validation, and a save action. An infolist is read-only and only displays state through entries — there's no input, no validation, and nothing to submit. In v4 both are built on the same underlying Schema, which is why entries and form fields share layout components and can even be mixed in one schema.

How do I show a record in a modal in Filament?

Add a ViewAction to your table's recordActions() array. By default it opens a modal containing the record's data as disabled form fields. To control exactly what the modal shows, pass entries to the action's ->schema() method — and to keep it identical to your view page, point both at a shared static method that returns the same array of components.

How do I format a value in a Filament infolist?

Chain formatting helpers onto the entry. TextEntry supports ->money('GBP'), ->dateTime('j M Y'), ->badge() with ->color(), ->copyable(), and ->limit(), among others; IconEntry::make('is_paid')->boolean() renders a tick or cross for boolean columns. These transform the displayed state without changing the underlying model attribute, so you don't need accessors or custom Blade.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.