Why Extend Filament at the Component Level
Filament ships with a rich set of fields and columns, but production panels inevitably need components that don't exist yet — a colour-swatch picker, a rich diff viewer, a sparkline column. The wrong move is to fork core or paste raw Blade into a resource. The right move is to build a proper plugin that can be versioned, tested, and shared.
This article covers three extension points: a custom Form field plugin, a custom Table column, and render hooks for surgical UI injection.
1. Custom Form Field Plugin
A Filament field is a class that extends Filament\Forms\Components\Field and pairs with a Blade view.
// src/Forms/Components/ColourSwatchPicker.php
namespace Acme\FilamentColour\Forms\Components;
use Filament\Forms\Components\Field;
class ColourSwatchPicker extends Field
{
protected string $view = 'filament-colour::forms.components.colour-swatch-picker';
/** @var array<string> */
protected array $swatches = [];
public function swatches(array $colours): static
{
$this->swatches = $colours;
return $this;
}
public function getSwatches(): array
{
return $this->swatches;
}
}
The Blade view receives $field automatically:
{{-- resources/views/forms/components/colour-swatch-picker.blade.php --}}
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div x-data="{ selected: @entangle($getStatePath()) }" class="flex gap-2">
@foreach ($field->getSwatches() as $colour)
<button
type="button"
:class="selected === '{{ $colour }}' ? 'ring-2 ring-offset-1' : ''"
style="background:{{ $colour }}"
class="w-8 h-8 rounded-full"
@click="selected = '{{ $colour }}'"
></button>
@endforeach
</div>
</x-dynamic-component>
Register it in your plugin's service provider so auto-discovery works:
public function packageBooted(): void
{
Filament::registerRenderHook('panels::body.end', fn () => '');
// Blade component registration happens via package view namespace
}
Usage in a resource:
ColourSwatchPicker::make('brand_colour')
->swatches(['#ef4444', '#3b82f6', '#22c55e'])
->required(),
2. Custom Table Column
Custom columns extend Filament\Tables\Columns\Column and follow the same view convention.
namespace Acme\FilamentColour\Tables\Columns;
use Filament\Tables\Columns\Column;
class ColourSwatchColumn extends Column
{
protected string $view = 'filament-colour::tables.columns.colour-swatch-column';
}
{{-- tables/columns/colour-swatch-column.blade.php --}}
<div
class="w-6 h-6 rounded-full border border-gray-200"
style="background: {{ $getState() }}"
title="{{ $getState() }}"
></div>
Because $getState() resolves through Filament's normal attribute pipeline, sorting, searching, and formatStateUsing() all work without extra effort.
ColourSwatchColumn::make('brand_colour')
->label('Brand')
->sortable(),
3. Render Hooks for Surgical UI Injection
Render hooks let you inject Blade output at named slots across every Filament panel without touching a single core file. They are registered in a service provider or panel provider.
use Filament\Support\Facades\FilamentView;
use Illuminate\Support\Facades\Blade;
FilamentView::registerRenderHook(
'panels::topbar.end',
fn (): string => Blade::render('<livewire:environment-badge />'),
);
Available hooks include panels::body.start, panels::sidebar.nav.start, panels::page.start, and many more — check the Filament docs for the full list per version.
For hooks that should only fire on specific pages, gate them:
use Filament\Pages\Page;
FilamentView::registerRenderHook(
'panels::page.end',
fn (): string => Blade::render('<livewire:audit-trail-drawer />'),
scopes: App\Filament\Resources\OrderResource\Pages\EditOrder::class,
);
Packaging It All Together
Wrap everything in a Spatie Laravel Package Tools plugin:
public function configurePackage(Package $package): void
{
$package
->name('filament-colour')
->hasViews()
->hasConfigFile();
}
Filament's own plugin interface (FilamentPlugin) lets you hook into panel registration cleanly, giving you access to the panel instance for conditional logic.
Key Takeaways
- Extend
FieldorColumnand pair with a namespaced Blade view — no hacks needed. @entangle($getStatePath())is the correct Alpine bridge for two-way field state.- Render hooks are scoped to panels and pages, keeping injection surgical and reversible.
- Package everything with Spatie Package Tools; Filament's auto-discovery handles the rest.
- Custom columns inherit sorting, searching, and
formatStateUsing()for free.