The Problem With Publishing Views
The moment you run php artisan vendor:publish --tag=filament-views you own those views forever. Every Filament upgrade becomes a manual diff exercise. Render hooks exist precisely to avoid that trap — they are named slots baked into Filament's own Blade templates where you can push arbitrary HTML, Livewire components, or Alpine snippets without touching a single vendor file.
How Render Hooks Work
Filament ships a FilamentView facade (backed by Filament\Support\Facades\FilamentView) that maintains a registry of closures keyed by hook name. At render time each Blade template calls @filamentRenderHook('hook.name'), which resolves and echoes every registered closure in order.
Registration lives in a PanelProvider or any service provider booted after Filament:
use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;
public function boot(): void
{
FilamentView::registerRenderHook(
PanelsRenderHook::BODY_START,
fn (): string => Blade::render('<livewire:impersonation-banner />'),
);
}
The closure must return a string or a Htmlable. Returning a View instance works too because View implements Htmlable:
FilamentView::registerRenderHook(
PanelsRenderHook::PAGE_HEADER_ACTIONS_BEFORE,
fn (): \Illuminate\Contracts\View\View =>
view('partials.environment-ribbon', ['env' => app()->environment()]),
);
Scoping Hooks to Specific Pages or Resources
Global hooks fire on every page. Pass a scopes array to limit execution:
use App\Filament\Resources\OrderResource\Pages\ListOrders;
use Filament\View\PanelsRenderHook;
FilamentView::registerRenderHook(
PanelsRenderHook::PAGE_FOOTER_WIDGETS_AFTER,
fn (): string => Blade::render('<livewire:order-export-progress />'),
scopes: [ListOrders::class],
);
Scopes accept any combination of page classes, resource classes, or widget classes. Filament resolves the current page class at render time and skips hooks whose scope does not match.
Key Hook Names in v4
Filament v4 consolidates hook names under PanelsRenderHook. The most useful ones:
| Constant | Location |
|---|---|
| BODY_START | Right after <body> |
| BODY_END | Right before </body> |
| SIDEBAR_NAV_START | Top of sidebar nav |
| SIDEBAR_NAV_END | Bottom of sidebar nav |
| PAGE_HEADER_ACTIONS_BEFORE | Before page header action buttons |
| PAGE_FOOTER_WIDGETS_AFTER | After footer widget grid |
| GLOBAL_SEARCH_START | Above the global search input |
| TOPBAR_START | Left side of the top bar |
Always reference the PanelsRenderHook class constants rather than raw strings — they are typed and refactor-safe.
Injecting a Livewire Component With Context
Closures receive the current $livewire component instance when Filament passes it. Declare it in the closure signature:
use Livewire\Component;
FilamentView::registerRenderHook(
PanelsRenderHook::PAGE_HEADER_ACTIONS_BEFORE,
function (Component $livewire): string {
if (! $livewire instanceof \App\Filament\Resources\InvoiceResource\Pages\EditInvoice) {
return '';
}
$id = $livewire->record?->getKey();
return Blade::render("<livewire:invoice-status-badge :invoice-id=\"$id\" />");
},
);
This pattern is cleaner than scopes when you need access to the record or route parameters.
Organising Hooks at Scale
Once you have more than a handful of hooks, extract them into dedicated classes:
// app/Filament/Hooks/ImpersonationHooks.php
class ImpersonationHooks
{
public static function register(): void
{
FilamentView::registerRenderHook(
PanelsRenderHook::BODY_START,
fn (): View => view('filament.hooks.impersonation-banner'),
);
}
}
// In PanelProvider::boot()
ImpersonationHooks::register();
Group by feature domain, not by hook position. This makes it trivial to disable an entire feature's UI injection in one line.
Takeaways
- Register hooks in
PanelProvider::boot()or any service provider; never publish core views. - Use
PanelsRenderHookconstants — not raw strings — for type safety. - Scope hooks to specific page or resource classes to avoid unnecessary rendering.
- Accept the
Component $livewireargument when you need record or route context. - Extract hook registrations into feature-scoped classes as the panel grows.