Why Render Hooks Exist
Every Filament panel ships with a fixed Blade layout. The temptation when you need a custom banner, a global notification bar, or a contextual help widget is to publish and edit vendor views. That path leads to painful upgrades. Render hooks are the sanctioned escape hatch: named slots scattered throughout the panel layout that you can fill from a service provider, a plugin, or a panel configuration closure.
Filament v4 expanded the hook inventory and tightened the scoping API, so it is worth understanding the full picture rather than cargo-culting a single example.
Registering a Hook
Hooks are registered via FilamentView::registerRenderHook(). The cleanest place is a PanelServiceProvider boot() method or inside the panel's ->renderHook() fluent call.
use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;
public function boot(): void
{
FilamentView::registerRenderHook(
PanelsRenderHook::BODY_START,
fn (): string => Blade::render('<x-app.environment-banner />'),
);
}
The closure must return a string or a Htmlable. Returning a rendered Blade component keeps logic in the component class and the hook registration thin.
Scoping to Specific Pages or Resources
Without scoping, a hook fires on every panel page. Filament v4 lets you pass a scopes array of fully-qualified page or resource class names as the third argument.
use App\Filament\Resources\OrderResource\Pages\EditOrder;
FilamentView::registerRenderHook(
PanelsRenderHook::RESOURCE_PAGES_EDIT_HEADER_WIDGETS_BEFORE,
fn (): string => Blade::render('<livewire:order-audit-trail :key="audit" />'),
scopes: [EditOrder::class],
);
This is the feature most developers miss. Without it you end up with conditional logic inside the closure, which is harder to test and harder to remove later.
Embedding a Full Livewire Component
For interactive widgets — think a floating support chat trigger or a real-time status badge — render a Livewire component tag. Filament's Livewire integration means the component hydrates normally.
FilamentView::registerRenderHook(
PanelsRenderHook::GLOBAL_SEARCH_AFTER,
fn (): Htmlable => new HtmlString(
Blade::render('@livewire("support-beacon")')
),
);
Keep the Livewire component itself stateless where possible. If it needs the current panel user, resolve it inside the component's mount() rather than passing it through the hook closure — closures run at render time but the panel auth context is already available.
Available Hook Constants
The PanelsRenderHook class is the canonical reference. A non-exhaustive selection of hooks you will reach for most often:
PanelsRenderHook::BODY_START
PanelsRenderHook::BODY_END
PanelsRenderHook::SIDEBAR_NAV_START
PanelsRenderHook::SIDEBAR_NAV_END
PanelsRenderHook::TOPBAR_START
PanelsRenderHook::TOPBAR_END
PanelsRenderHook::PAGE_START
PanelsRenderHook::PAGE_END
PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABLE_BEFORE
PanelsRenderHook::RESOURCE_PAGES_EDIT_HEADER_WIDGETS_BEFORE
For non-panel contexts (tables, forms, notifications) Filament exposes parallel constants in TablesRenderHook, FormsRenderHook, and NotificationsRenderHook.
Testing a Render Hook
Render hooks are exercised through standard Livewire/Filament page tests. Assert the rendered HTML contains your injected markup:
use function Pest\Livewire\livewire;
use App\Filament\Resources\OrderResource\Pages\EditOrder;
it('renders the audit trail on the edit order page', function () {
$order = Order::factory()->create();
livewire(EditOrder::class, ['record' => $order->getRouteKey()])
->assertSeeHtml('data-testid="audit-trail"');
});
If the hook is scoped, test it on the scoped page and assert it is absent on an unscoped page to confirm the scope is working.
Takeaways
- Register hooks in
boot()or the panel fluent API — never edit vendor views. - Always pass
scopeswhen the injection is page-specific; it prevents unintended side effects. - Return
Htmlableor a plainstring; avoid returning objects that Blade cannot coerce. - Livewire components inside hooks hydrate normally — keep them stateless and resolve auth inside
mount(). - Use
PanelsRenderHookconstants; string literals will break on upgrades. - Cover hooks with page-level Pest assertions to catch regressions early.