Why Filament v4 Rewrote the Component Model
In Filament v3 you maintained two parallel trees: form(Form $form) returned $form->schema([...]) and infolist(Infolist $infolist) returned $infolist->schema([...]).
The components were different classes even when they displayed the same data — a TextInput for editing, a TextEntry for viewing.
Filament v4 collapses this into a unified Schema API where a single component tree can render in both contexts, and dedicated entry components are first-class citizens alongside field components.
The Schema Component Tree
Every layout wrapper — Section, Grid, Tabs, Fieldset — now lives under Filament\Schemas\Components\ and is shared between forms and infolists.
You import them once and use them everywhere.
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Grid;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
public static function form(Form $form): Form
{
return $form->schema([
Section::make('Identity')
->schema([
Grid::make(2)->schema([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
]),
]),
]);
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
Section::make('Identity')
->schema([
Grid::make(2)->schema([
TextEntry::make('name'),
TextEntry::make('email'),
]),
]),
]);
}
The Section and Grid imports are identical. Only the leaf components differ.
Extracting a Shared Schema Method
Because layout wrappers are now the same class, you can extract the skeleton into a static helper and swap only the leaves.
private static function identitySchema(array $fields): array
{
return [
Section::make('Identity')
->schema([
Grid::make(2)->schema($fields),
]),
];
}
public static function form(Form $form): Form
{
return $form->schema(self::identitySchema([
TextInput::make('name')->required(),
TextInput::make('email')->email()->required(),
]));
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist->schema(self::identitySchema([
TextEntry::make('name'),
TextEntry::make('email'),
]));
}
This pattern eliminates the structural duplication that plagued v3 resources with large schemas.
Schema Components in Custom Pages and Widgets
Custom pages that previously called $this->form->fill() now use HasForms or HasInfolists traits alongside the schema builder.
The $this->form(...) and $this->infolist(...) calls accept the same unified component classes.
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Schemas\Components\Section;
class EditProfilePage extends Page
{
use InteractsWithForms;
public function form(Form $form): Form
{
return $form
->schema([
Section::make()->schema([
TextInput::make('bio')->columnSpanFull(),
]),
])
->statePath('data');
}
}
What Actually Breaks When Upgrading
Namespace changes
Any import of Filament\Forms\Components\Section or Filament\Forms\Components\Grid must move to Filament\Schemas\Components\. A project-wide find-and-replace handles most of it.
->columns() on Section
In v3 you called ->columns(2) on Section directly. In v4 you wrap children in Grid::make(2) instead. The old shorthand still works as a compatibility shim in early v4 releases, but the canonical approach is explicit Grid.
Custom field getChildComponents()
If you built custom layout components by extending Filament\Forms\Components\Component, the base class has moved. Extend Filament\Schemas\Components\Component instead and implement getChildComponents() as before.
Takeaways
- Layout components (
Section,Grid,Tabs) are now shared across forms and infolists underFilament\Schemas\Components\. - Leaf components (
TextInput,TextEntry) remain context-specific but sit inside the same tree. - Extracting a shared schema skeleton method removes structural duplication across
form()andinfolist(). - The main upgrade cost is namespace replacement and swapping
->columns()shortcuts for explicitGridwrappers. - Custom layout components must extend the new base class in
Filament\Schemas\Components\.